threadroot 0.1.1 → 0.1.3

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 (4) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +34 -23
  3. package/dist/index.js +1942 -1251
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,121 +3,24 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
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
- ]);
6
+ // src/core/bootstrap.ts
7
+ import { stat as stat9 } from "fs/promises";
8
+ import path23 from "path";
24
9
 
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).default([]),
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
- });
10
+ // src/core/doctor.ts
11
+ import { stat as stat5 } from "fs/promises";
12
+ import path15 from "path";
116
13
 
117
- // src/core/harness/paths.ts
118
- import os from "os";
14
+ // src/core/compile/index.ts
15
+ import { readFile, stat } from "fs/promises";
119
16
  import path2 from "path";
120
17
 
18
+ // src/core/hash.ts
19
+ import { createHash } from "crypto";
20
+ function hashContent(content) {
21
+ return createHash("sha256").update(content).digest("hex");
22
+ }
23
+
121
24
  // src/core/paths.ts
122
25
  import path from "path";
123
26
  function toRepoPath(repoRoot, relativePath) {
@@ -133,430 +36,68 @@ function toRepoPath(repoRoot, relativePath) {
133
36
  return resolved;
134
37
  }
135
38
 
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"
39
+ // src/core/compile/adapters/agents.ts
40
+ var agentsAdapter = {
41
+ id: "agents",
42
+ compile(ctx) {
43
+ return [{ path: "AGENTS.md", content: ctx.canonicalAgents }];
44
+ }
150
45
  };
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
46
 
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() };
47
+ // src/core/compile/adapters/shared.ts
48
+ function slug(name) {
49
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "rule";
50
+ }
51
+ function ruleBody(rule) {
52
+ const body = rule.body.trim();
53
+ if (body.startsWith("#")) {
54
+ return body;
181
55
  }
182
- const parsed = parseYaml(match[1] ?? "");
183
- const data = parsed && typeof parsed === "object" ? parsed : {};
184
- return { data, body: (match[2] ?? "").trim() };
56
+ return `# ${rule.name}
57
+
58
+ ${body}`;
185
59
  }
186
- function serializeFrontmatter(data, body) {
187
- const front = stringifyYaml(data).trimEnd();
188
- return `---
189
- ${front}
190
- ---
191
60
 
192
- ${body.trim()}
61
+ // src/core/compile/adapters/claude.ts
62
+ function claudeRoot() {
63
+ const lines = [
64
+ "# CLAUDE.md",
65
+ "",
66
+ "@AGENTS.md",
67
+ "",
68
+ "<!-- The shared harness lives in AGENTS.md (imported above). Claude-specific",
69
+ " guidance can be added below this comment; it is preserved across compiles. -->"
70
+ ];
71
+ return `${lines.join("\n")}
193
72
  `;
194
73
  }
74
+ function ruleFile(name, applyTo, body) {
75
+ const frontmatter = ["---", `name: ${name}`, "paths:", ` - "${applyTo}"`, "---", ""].join("\n");
76
+ return { path: `.claude/rules/${slug(name)}.md`, content: `${frontmatter}
77
+ ${body}
78
+ ` };
79
+ }
80
+ function commandFile(name, description, run2, script) {
81
+ const frontmatter = ["---", `description: ${description}`, "---", ""].join("\n");
82
+ const invocation = run2 ? `Run this shell command and report the result:
195
83
 
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
- );
84
+ \`\`\`sh
85
+ ${run2}
86
+ \`\`\`` : `Run the script at \`.threadroot/tools/${script}\` and report the result.`;
87
+ const body = [`# /${slug(name)}`, "", description, "", invocation].join("\n");
88
+ return { path: `.claude/commands/${slug(name)}.md`, content: `${frontmatter}
89
+ ${body}
90
+ ` };
223
91
  }
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
- }
92
+ var claudeAdapter = {
93
+ id: "claude",
94
+ compile(ctx) {
95
+ const files = [{ path: "CLAUDE.md", content: claudeRoot() }];
96
+ for (const rule of ctx.scopedRules) {
97
+ const applyTo = rule.frontmatter.applyTo;
98
+ if (applyTo) {
99
+ files.push(ruleFile(rule.name, applyTo, ruleBody(rule)));
100
+ }
560
101
  }
561
102
  for (const tool of ctx.tools) {
562
103
  files.push(
@@ -795,7 +336,7 @@ function isGlob(value) {
795
336
  }
796
337
  async function readIfExists(filePath) {
797
338
  try {
798
- return await readFile3(filePath, "utf8");
339
+ return await readFile(filePath, "utf8");
799
340
  } catch (error) {
800
341
  if (error.code === "ENOENT") {
801
342
  return void 0;
@@ -814,7 +355,7 @@ async function resolveReference(repoRoot, reference) {
814
355
  return { reference, exists: true };
815
356
  }
816
357
  if (reference.load === "eager" && info.size <= EAGER_INLINE_LIMIT) {
817
- const inlined = await readFile3(absolute, "utf8");
358
+ const inlined = await readFile(absolute, "utf8");
818
359
  return { reference, exists: true, inlined };
819
360
  }
820
361
  return { reference, exists: true };
@@ -826,7 +367,7 @@ async function resolveReference(repoRoot, reference) {
826
367
  }
827
368
  }
828
369
  async function buildContext(repoRoot, harness) {
829
- const existingAgents = await readIfExists(path5.join(repoRoot, AGENTS_FILE)) ?? "";
370
+ const existingAgents = await readIfExists(path2.join(repoRoot, AGENTS_FILE)) ?? "";
830
371
  const handAuthored = extractHandAuthored(existingAgents);
831
372
  const { global, scoped } = splitRules(harness.rules);
832
373
  const references = await Promise.all(
@@ -858,201 +399,505 @@ async function compile(repoRoot, harness) {
858
399
  if (!adapter) {
859
400
  continue;
860
401
  }
861
- for (const file of adapter.compile(ctx)) {
862
- if (seen.has(file.path)) {
863
- continue;
402
+ for (const file of adapter.compile(ctx)) {
403
+ if (seen.has(file.path)) {
404
+ continue;
405
+ }
406
+ seen.add(file.path);
407
+ files.push(file);
408
+ }
409
+ }
410
+ files.sort((a, b) => a.path.localeCompare(b.path));
411
+ return files;
412
+ }
413
+ async function detectDrift(repoRoot, files) {
414
+ const entries = await Promise.all(
415
+ files.map(async (file) => {
416
+ const existing = await readIfExists(path2.join(repoRoot, file.path));
417
+ if (existing === void 0) {
418
+ return { path: file.path, status: "create" };
419
+ }
420
+ const status = hashContent(existing) === hashContent(file.content) ? "unchanged" : "drift";
421
+ return { path: file.path, status };
422
+ })
423
+ );
424
+ return entries;
425
+ }
426
+
427
+ // src/core/harness/schema.ts
428
+ import { z as z2 } from "zod";
429
+
430
+ // src/types.ts
431
+ import { z } from "zod";
432
+ var profileIdSchema = z.enum([
433
+ "nextjs",
434
+ "vite-react",
435
+ "fastapi",
436
+ "python-cli",
437
+ "node-cli",
438
+ "dbt",
439
+ "empty"
440
+ ]);
441
+
442
+ // src/core/harness/schema.ts
443
+ var objectScopeSchema = z2.enum(["user", "project"]);
444
+ var riskLevelSchema = z2.enum(["low", "medium", "high"]);
445
+ var adapterIdSchema = z2.enum(["agents", "claude", "copilot", "cursor"]);
446
+ var memoryTypeSchema = z2.enum(["project", "repo-map", "current-focus", "handoff", "pitfalls"]);
447
+ var referenceSchema = z2.object({
448
+ path: z2.string().min(1),
449
+ description: z2.string().optional(),
450
+ load: z2.enum(["link", "eager"]).default("link")
451
+ });
452
+ var harnessManifestSchema = z2.object({
453
+ name: z2.string().min(1),
454
+ version: z2.literal(1),
455
+ profile: profileIdSchema,
456
+ adapters: z2.array(adapterIdSchema).default([]),
457
+ references: z2.array(referenceSchema).default([]),
458
+ memory: z2.object({
459
+ budget: z2.record(memoryTypeSchema, z2.number().int().positive()).default({})
460
+ }).default({ budget: {} }),
461
+ tools: z2.object({
462
+ allow: z2.array(z2.string()).default([])
463
+ }).default({ allow: [] })
464
+ });
465
+ var skillFrontmatterSchema = z2.object({
466
+ name: z2.string().min(1),
467
+ description: z2.string().min(1).optional(),
468
+ when: z2.string().min(1).optional(),
469
+ license: z2.string().min(1).optional(),
470
+ compatibility: z2.string().max(500).optional(),
471
+ metadata: z2.record(z2.unknown()).optional(),
472
+ allowedTools: z2.union([z2.string(), z2.array(z2.string())]).optional(),
473
+ "allowed-tools": z2.union([z2.string(), z2.array(z2.string())]).optional(),
474
+ scope: objectScopeSchema.default("project"),
475
+ tags: z2.array(z2.string()).default([])
476
+ }).refine((skill) => Boolean(skill.description ?? skill.when), {
477
+ message: "A skill must define `description` or legacy `when`.",
478
+ path: ["description"]
479
+ }).transform((skill) => {
480
+ const trigger = skill.description ?? skill.when;
481
+ const allowedTools = skill.allowedTools ?? skill["allowed-tools"];
482
+ return {
483
+ ...skill,
484
+ description: skill.description ?? trigger,
485
+ when: skill.when ?? trigger,
486
+ allowedTools,
487
+ "allowed-tools": void 0
488
+ };
489
+ });
490
+ var ruleFrontmatterSchema = z2.object({
491
+ name: z2.string().min(1),
492
+ applyTo: z2.string().min(1).optional(),
493
+ scope: objectScopeSchema.default("project")
494
+ });
495
+ var toolInputParamSchema = z2.object({
496
+ type: z2.enum(["string", "number", "boolean"]).default("string"),
497
+ description: z2.string().optional(),
498
+ default: z2.union([z2.string(), z2.number(), z2.boolean()]).optional()
499
+ });
500
+ var healthcheckSchema = z2.object({
501
+ run: z2.string().min(1),
502
+ expectExitCode: z2.number().int().default(0)
503
+ });
504
+ var toolManifestSchema = z2.object({
505
+ name: z2.string().min(1),
506
+ description: z2.string().min(1),
507
+ scope: objectScopeSchema.default("project"),
508
+ risk: riskLevelSchema.default("low"),
509
+ confirm: z2.boolean().default(false),
510
+ connection: z2.string().min(1).optional(),
511
+ healthcheck: healthcheckSchema.optional(),
512
+ input: z2.record(z2.string(), toolInputParamSchema).default({}),
513
+ run: z2.string().min(1).optional(),
514
+ script: z2.string().min(1).optional()
515
+ }).refine((tool) => Boolean(tool.run) !== Boolean(tool.script), {
516
+ message: "A tool must define exactly one of `run` or `script`.",
517
+ path: ["run"]
518
+ });
519
+ var connectionManifestSchema = z2.object({
520
+ name: z2.string().min(1),
521
+ provider: z2.string().min(1),
522
+ kind: z2.literal("cli").default("cli"),
523
+ command: z2.string().min(1),
524
+ profile: z2.string().min(1).optional(),
525
+ description: z2.string().min(1),
526
+ scope: objectScopeSchema.default("project"),
527
+ risk: riskLevelSchema.default("medium"),
528
+ confirm: z2.boolean().default(false),
529
+ healthcheck: healthcheckSchema.optional(),
530
+ allow: z2.array(z2.string()).default([]),
531
+ deny: z2.array(z2.string()).default([])
532
+ });
533
+
534
+ // src/core/harness/paths.ts
535
+ import os from "os";
536
+ import path3 from "path";
537
+ var HARNESS_DIR = ".threadroot";
538
+ var HARNESS_MANIFEST = "harness.yaml";
539
+ var LOCK_FILE = "lock.json";
540
+ var HARNESS_SUBDIRS = {
541
+ skills: "skills",
542
+ tools: "tools",
543
+ connections: "connections",
544
+ rules: "rules",
545
+ memory: "memory"
546
+ };
547
+ var HARNESS_OBJECT_EXT = {
548
+ prose: ".md",
549
+ tool: ".yaml"
550
+ };
551
+ function projectHarnessDir(repoRoot) {
552
+ return toRepoPath(repoRoot, HARNESS_DIR);
553
+ }
554
+ function projectManifestPath(repoRoot) {
555
+ return toRepoPath(repoRoot, path3.join(HARNESS_DIR, HARNESS_MANIFEST));
556
+ }
557
+ function projectLockPath(repoRoot) {
558
+ return toRepoPath(repoRoot, path3.join(HARNESS_DIR, LOCK_FILE));
559
+ }
560
+ function projectObjectDir(repoRoot, dir) {
561
+ return toRepoPath(repoRoot, path3.join(HARNESS_DIR, HARNESS_SUBDIRS[dir]));
562
+ }
563
+ function userHarnessDir(home = os.homedir()) {
564
+ return path3.join(home, HARNESS_DIR);
565
+ }
566
+ function userObjectDir(dir, home = os.homedir()) {
567
+ return path3.join(userHarnessDir(home), HARNESS_SUBDIRS[dir]);
568
+ }
569
+ function userLockPath(home = os.homedir()) {
570
+ return path3.join(userHarnessDir(home), LOCK_FILE);
571
+ }
572
+
573
+ // src/core/harness/frontmatter.ts
574
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
575
+ var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
576
+ function parseFrontmatter(raw) {
577
+ const normalized = raw.replace(/^\uFEFF/, "");
578
+ const match = FRONTMATTER_RE.exec(normalized);
579
+ if (!match) {
580
+ return { data: {}, body: normalized.trim() };
581
+ }
582
+ const parsed = parseYaml(match[1] ?? "");
583
+ const data = parsed && typeof parsed === "object" ? parsed : {};
584
+ return { data, body: (match[2] ?? "").trim() };
585
+ }
586
+ function serializeFrontmatter(data, body) {
587
+ const front = stringifyYaml(data).trimEnd();
588
+ return `---
589
+ ${front}
590
+ ---
591
+
592
+ ${body.trim()}
593
+ `;
594
+ }
595
+
596
+ // src/core/harness/load.ts
597
+ import { readFile as readFile2, readdir } from "fs/promises";
598
+ import path4 from "path";
599
+ import { parse as parseYaml2 } from "yaml";
600
+ var HarnessError = class extends Error {
601
+ constructor(message) {
602
+ super(message);
603
+ this.name = "HarnessError";
604
+ }
605
+ };
606
+ async function readObjectFiles(dir, ext) {
607
+ let entries;
608
+ try {
609
+ entries = await readdir(dir);
610
+ } catch (error) {
611
+ if (error.code === "ENOENT") {
612
+ return [];
613
+ }
614
+ throw error;
615
+ }
616
+ const files = entries.filter((name) => name.endsWith(ext)).sort();
617
+ return Promise.all(
618
+ files.map(async (name) => {
619
+ const full = path4.join(dir, name);
620
+ return { path: full, content: await readFile2(full, "utf8") };
621
+ })
622
+ );
623
+ }
624
+ async function readSkillFiles(dir) {
625
+ let entries;
626
+ try {
627
+ entries = await readdir(dir, { withFileTypes: true });
628
+ } catch (error) {
629
+ if (error.code === "ENOENT") {
630
+ return [];
631
+ }
632
+ throw error;
633
+ }
634
+ const files = [];
635
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
636
+ if (entry.isFile() && entry.name.endsWith(HARNESS_OBJECT_EXT.prose)) {
637
+ const full = path4.join(dir, entry.name);
638
+ files.push({ path: full, content: await readFile2(full, "utf8") });
639
+ continue;
640
+ }
641
+ if (entry.isDirectory()) {
642
+ const full = path4.join(dir, entry.name, "SKILL.md");
643
+ try {
644
+ files.push({ path: full, content: await readFile2(full, "utf8") });
645
+ } catch (error) {
646
+ if (error.code !== "ENOENT") {
647
+ throw error;
648
+ }
864
649
  }
865
- seen.add(file.path);
866
- files.push(file);
867
650
  }
868
651
  }
869
- files.sort((a, b) => a.path.localeCompare(b.path));
870
652
  return files;
871
653
  }
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;
654
+ function objectDirFor(repoRoot, dir, origin, home) {
655
+ return origin === "project" ? projectObjectDir(repoRoot, dir) : userObjectDir(dir, home);
884
656
  }
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);
657
+ function describe(error) {
658
+ if (error && typeof error === "object" && "issues" in error) {
659
+ const issues = error.issues;
660
+ return issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
661
+ }
662
+ return error instanceof Error ? error.message : String(error);
896
663
  }
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 };
664
+ async function loadSkillsFrom(dir, origin) {
665
+ const files = await readSkillFiles(dir);
666
+ return files.map((file) => {
667
+ const { data, body } = parseFrontmatter(file.content);
668
+ const result = skillFrontmatterSchema.safeParse(data);
669
+ if (!result.success) {
670
+ throw new HarnessError(`Invalid skill ${file.path}: ${describe(result.error)}`);
671
+ }
672
+ return { name: result.data.name, origin, sourcePath: file.path, frontmatter: result.data, body };
673
+ });
904
674
  }
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}`);
675
+ async function loadRulesFrom(dir, origin) {
676
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.prose);
677
+ return files.map((file) => {
678
+ const { data, body } = parseFrontmatter(file.content);
679
+ const result = ruleFrontmatterSchema.safeParse(data);
680
+ if (!result.success) {
681
+ throw new HarnessError(`Invalid rule ${file.path}: ${describe(result.error)}`);
915
682
  }
916
- } catch (error) {
917
- if (error instanceof HarnessError) {
918
- console.error(error.message);
919
- process.exitCode = 1;
920
- return;
683
+ return { name: result.data.name, origin, sourcePath: file.path, frontmatter: result.data, body };
684
+ });
685
+ }
686
+ async function loadToolsFrom(dir, origin) {
687
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.tool);
688
+ return files.map((file) => {
689
+ const parsed = parseYaml2(file.content);
690
+ const result = toolManifestSchema.safeParse(parsed);
691
+ if (!result.success) {
692
+ throw new HarnessError(`Invalid tool ${file.path}: ${describe(result.error)}`);
921
693
  }
922
- throw error;
694
+ return { name: result.data.name, origin, sourcePath: file.path, manifest: result.data };
695
+ });
696
+ }
697
+ async function loadConnectionsFrom(dir, origin) {
698
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.tool);
699
+ return files.map((file) => {
700
+ const parsed = parseYaml2(file.content);
701
+ const result = connectionManifestSchema.safeParse(parsed);
702
+ if (!result.success) {
703
+ throw new HarnessError(`Invalid connection ${file.path}: ${describe(result.error)}`);
704
+ }
705
+ return { name: result.data.name, origin, sourcePath: file.path, manifest: result.data };
706
+ });
707
+ }
708
+ async function loadMemoryFrom(dir, origin) {
709
+ const files = await readObjectFiles(dir, HARNESS_OBJECT_EXT.prose);
710
+ const memory = [];
711
+ for (const file of files) {
712
+ const base = path4.basename(file.path, HARNESS_OBJECT_EXT.prose);
713
+ const type = memoryTypeSchema.safeParse(base);
714
+ if (!type.success) {
715
+ continue;
716
+ }
717
+ const { body } = parseFrontmatter(file.content);
718
+ memory.push({ type: type.data, origin, sourcePath: file.path, body });
923
719
  }
720
+ return memory;
924
721
  }
925
-
926
- // src/commands/context.ts
927
- async function runContext(repoRoot, task) {
928
- let context;
722
+ function mergeByName(user, project) {
723
+ const merged = /* @__PURE__ */ new Map();
724
+ for (const item of user) {
725
+ merged.set(item.name, item);
726
+ }
727
+ for (const item of project) {
728
+ merged.set(item.name, item);
729
+ }
730
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
731
+ }
732
+ async function loadManifest(repoRoot) {
733
+ const manifestPath = projectManifestPath(repoRoot);
734
+ let raw;
929
735
  try {
930
- context = await assembleContext(repoRoot, task);
736
+ raw = await readFile2(manifestPath, "utf8");
931
737
  } catch (error) {
932
- if (error instanceof HarnessError) {
933
- console.log("No harness found. Run `tr init` first.");
934
- return;
738
+ if (error.code === "ENOENT") {
739
+ throw new HarnessError(`No harness found at ${manifestPath}. Run \`tr init\` first.`);
935
740
  }
936
741
  throw error;
937
742
  }
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
- }
743
+ const result = harnessManifestSchema.safeParse(parseYaml2(raw));
744
+ if (!result.success) {
745
+ throw new HarnessError(`Invalid ${manifestPath}: ${describe(result.error)}`);
956
746
  }
957
- if (context.memory.length > 0) {
958
- console.log("\nmemory:");
959
- for (const entry of context.memory) {
960
- console.log(`- ${entry.type}`);
961
- }
747
+ return result.data;
748
+ }
749
+ async function resolveHarness(repoRoot, opts = {}) {
750
+ const { home } = opts;
751
+ const manifest = await loadManifest(repoRoot);
752
+ const [
753
+ userSkills,
754
+ projectSkills,
755
+ userRules,
756
+ projectRules,
757
+ userTools,
758
+ projectTools,
759
+ userConnections,
760
+ projectConnections,
761
+ userMemory,
762
+ projectMemory
763
+ ] = await Promise.all([
764
+ loadSkillsFrom(objectDirFor(repoRoot, "skills", "user", home), "user"),
765
+ loadSkillsFrom(objectDirFor(repoRoot, "skills", "project", home), "project"),
766
+ loadRulesFrom(objectDirFor(repoRoot, "rules", "user", home), "user"),
767
+ loadRulesFrom(objectDirFor(repoRoot, "rules", "project", home), "project"),
768
+ loadToolsFrom(objectDirFor(repoRoot, "tools", "user", home), "user"),
769
+ loadToolsFrom(objectDirFor(repoRoot, "tools", "project", home), "project"),
770
+ loadConnectionsFrom(objectDirFor(repoRoot, "connections", "user", home), "user"),
771
+ loadConnectionsFrom(objectDirFor(repoRoot, "connections", "project", home), "project"),
772
+ loadMemoryFrom(objectDirFor(repoRoot, "memory", "user", home), "user"),
773
+ loadMemoryFrom(objectDirFor(repoRoot, "memory", "project", home), "project")
774
+ ]);
775
+ return {
776
+ manifest,
777
+ skills: mergeByName(userSkills, projectSkills),
778
+ rules: mergeByName(userRules, projectRules),
779
+ tools: mergeByName(userTools, projectTools),
780
+ connections: mergeByName(userConnections, projectConnections),
781
+ memory: [...userMemory, ...projectMemory]
782
+ };
783
+ }
784
+
785
+ // src/core/harness/context.ts
786
+ function taskTerms(task) {
787
+ return [
788
+ ...new Set(
789
+ task.toLowerCase().split(/[^a-z0-9+#.-]+/).filter((term) => term.length > 2)
790
+ )
791
+ ];
792
+ }
793
+ function scoreSkill(haystack, terms) {
794
+ const lower = haystack.toLowerCase();
795
+ return terms.reduce((score, term) => score + (lower.includes(term) ? 1 : 0), 0);
796
+ }
797
+ async function assembleContext(repoRoot, task, options = {}) {
798
+ const harness = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
799
+ const terms = taskTerms(task);
800
+ let ranked = harness.skills.map((skill) => ({
801
+ skill,
802
+ score: scoreSkill(`${skill.name} ${skill.frontmatter.when} ${skill.frontmatter.tags.join(" ")}`, terms)
803
+ })).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 }) => ({
804
+ name: skill.name,
805
+ when: skill.frontmatter.when,
806
+ tags: skill.frontmatter.tags,
807
+ scope: skill.frontmatter.scope,
808
+ sourcePath: skill.sourcePath,
809
+ score
810
+ }));
811
+ if (ranked.length === 0 && options.fallbackSkills) {
812
+ ranked = harness.skills.slice(0, options.limit ?? 8).map((skill) => ({
813
+ name: skill.name,
814
+ when: skill.frontmatter.when,
815
+ tags: skill.frontmatter.tags,
816
+ scope: skill.frontmatter.scope,
817
+ sourcePath: skill.sourcePath,
818
+ score: 0
819
+ }));
962
820
  }
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.");
821
+ return {
822
+ task,
823
+ skills: ranked,
824
+ rules: harness.rules.map((rule) => ({ name: rule.name, applyTo: rule.frontmatter.applyTo })),
825
+ tools: harness.tools.map((tool) => ({
826
+ name: tool.name,
827
+ description: tool.manifest.description,
828
+ confirm: tool.manifest.confirm,
829
+ risk: tool.manifest.risk,
830
+ connection: tool.manifest.connection,
831
+ healthcheck: Boolean(tool.manifest.healthcheck),
832
+ kind: tool.manifest.run ? "shell" : "script"
833
+ })),
834
+ memory: harness.memory.map((entry) => ({ type: entry.type, body: entry.body }))
835
+ };
836
+ }
837
+
838
+ // src/core/harness/memory.ts
839
+ import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
840
+ import path5 from "path";
841
+ function assertMemoryType(type) {
842
+ const parsed = memoryTypeSchema.safeParse(type);
843
+ if (!parsed.success) {
844
+ throw new HarnessError(
845
+ `Unknown memory type \`${type}\`. Expected one of: ${memoryTypeSchema.options.join(", ")}.`
846
+ );
965
847
  }
848
+ return parsed.data;
849
+ }
850
+ function memoryDir(repoRoot, scope, home) {
851
+ return scope === "project" ? projectObjectDir(repoRoot, "memory") : userObjectDir("memory", home);
966
852
  }
967
-
968
- // src/commands/diff.ts
969
- import fs from "fs/promises";
970
- import path7 from "path";
971
- async function readIfExists2(filePath) {
853
+ function memoryFilePath(repoRoot, type, scope = "project", home) {
854
+ return path5.join(memoryDir(repoRoot, scope, home), `${type}${HARNESS_OBJECT_EXT.prose}`);
855
+ }
856
+ async function readMemory(repoRoot, type, options = {}) {
857
+ const memoryType = assertMemoryType(type);
858
+ const file = memoryFilePath(repoRoot, memoryType, options.scope ?? "project", options.home);
972
859
  try {
973
- return await fs.readFile(filePath, "utf8");
860
+ const raw = await readFile3(file, "utf8");
861
+ return parseFrontmatter(raw).body;
974
862
  } catch (error) {
975
863
  if (error.code === "ENOENT") {
976
- return void 0;
864
+ return null;
977
865
  }
978
866
  throw error;
979
867
  }
980
868
  }
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;
869
+ function headingFor(type) {
870
+ const title = type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
871
+ return `# ${title}`;
1014
872
  }
1015
- async function runDiff(repoRoot) {
1016
- let harness;
873
+ async function appendMemory(repoRoot, type, note, options = {}) {
874
+ const memoryType = assertMemoryType(type);
875
+ const trimmed = note.trim();
876
+ if (!trimmed) {
877
+ throw new HarnessError("Cannot append an empty memory note.");
878
+ }
879
+ const scope = options.scope ?? "project";
880
+ const dir = memoryDir(repoRoot, scope, options.home);
881
+ const file = memoryFilePath(repoRoot, memoryType, scope, options.home);
882
+ let existing = "";
1017
883
  try {
1018
- harness = await resolveHarness(repoRoot);
884
+ existing = await readFile3(file, "utf8");
1019
885
  } 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}`);
886
+ if (error.code !== "ENOENT") {
887
+ throw error;
1042
888
  }
1043
889
  }
1044
- if (changed === 0) {
1045
- console.log("No drift: optional compiled outputs match the canonical harness.");
1046
- }
890
+ await mkdir(dir, { recursive: true });
891
+ const base = existing.trim() ? existing.replace(/\s*$/, "") : headingFor(memoryType);
892
+ await writeFile(file, `${base}
893
+ - ${trimmed}
894
+ `, "utf8");
895
+ return { type: memoryType, scope, path: file };
1047
896
  }
1048
897
 
1049
- // src/core/doctor.ts
1050
- import { stat as stat5 } from "fs/promises";
1051
- import path16 from "path";
1052
-
1053
898
  // src/core/install/lock.ts
1054
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1055
- import path8 from "path";
899
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
900
+ import path6 from "path";
1056
901
 
1057
902
  // src/core/install/source.ts
1058
903
  import { z as z3 } from "zod";
@@ -1152,8 +997,8 @@ async function writeLockFile(lockPath, lock) {
1152
997
  version: lock.version,
1153
998
  objects: [...lock.objects].sort((a, b) => a.name.localeCompare(b.name))
1154
999
  };
1155
- await mkdir3(path8.dirname(lockPath), { recursive: true });
1156
- await writeFile3(lockPath, `${JSON.stringify(sorted, null, 2)}
1000
+ await mkdir2(path6.dirname(lockPath), { recursive: true });
1001
+ await writeFile2(lockPath, `${JSON.stringify(sorted, null, 2)}
1157
1002
  `, "utf8");
1158
1003
  }
1159
1004
  function upsertLockEntry(lock, entry) {
@@ -1182,7 +1027,7 @@ function externalSkillNames(lock) {
1182
1027
 
1183
1028
  // src/core/skills.ts
1184
1029
  import { readFile as readFile5, readdir as readdir2, stat as stat2 } from "fs/promises";
1185
- import path9 from "path";
1030
+ import path7 from "path";
1186
1031
  var SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/;
1187
1032
  var MIN_DESCRIPTION_LENGTH = 40;
1188
1033
  var MAX_DESCRIPTION_LENGTH = 1024;
@@ -1197,8 +1042,8 @@ function validateResolvedSkills(harness) {
1197
1042
  if (!SKILL_NAME_RE.test(skill.name)) {
1198
1043
  findings.push(finding("error", skill, "Skill names must use lowercase letters, digits, and hyphens."));
1199
1044
  }
1200
- if (path9.basename(skill.sourcePath) === "SKILL.md") {
1201
- const folderName = path9.basename(path9.dirname(skill.sourcePath));
1045
+ if (path7.basename(skill.sourcePath) === "SKILL.md") {
1046
+ const folderName = path7.basename(path7.dirname(skill.sourcePath));
1202
1047
  if (folderName !== skill.name) {
1203
1048
  findings.push(finding("error", skill, "Folder-based skill directory must match frontmatter `name`."));
1204
1049
  }
@@ -1263,14 +1108,14 @@ async function exists(filePath) {
1263
1108
  }
1264
1109
  }
1265
1110
  async function validateSkillDirectory(skill) {
1266
- if (path9.basename(skill.sourcePath) !== "SKILL.md") {
1111
+ if (path7.basename(skill.sourcePath) !== "SKILL.md") {
1267
1112
  return [];
1268
1113
  }
1269
1114
  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");
1115
+ const skillDir = path7.dirname(skill.sourcePath);
1116
+ const referencesDir = path7.join(skillDir, "references");
1117
+ const scriptsDir = path7.join(skillDir, "scripts");
1118
+ const evalsDir = path7.join(skillDir, "evals");
1274
1119
  if (await exists(scriptsDir)) {
1275
1120
  pushPathFinding(
1276
1121
  findings,
@@ -1285,11 +1130,11 @@ async function validateSkillDirectory(skill) {
1285
1130
  if (/^[a-z]+:\/\//i.test(target) || target.startsWith("#")) {
1286
1131
  continue;
1287
1132
  }
1288
- if (path9.isAbsolute(target) || target.split(/[\\/]/).includes("..")) {
1133
+ if (path7.isAbsolute(target) || target.split(/[\\/]/).includes("..")) {
1289
1134
  pushPathFinding(findings, "error", skill, `Skill link must stay inside the skill directory: ${target}`);
1290
1135
  continue;
1291
1136
  }
1292
- const resolved = path9.join(skillDir, target);
1137
+ const resolved = path7.join(skillDir, target);
1293
1138
  if (!await exists(resolved)) {
1294
1139
  pushPathFinding(findings, "error", skill, `Skill links to missing file: ${target}`);
1295
1140
  }
@@ -1304,7 +1149,7 @@ async function validateSkillDirectory(skill) {
1304
1149
  if (!entry.isFile()) {
1305
1150
  continue;
1306
1151
  }
1307
- const filePath = path9.join(referencesDir, entry.name);
1152
+ const filePath = path7.join(referencesDir, entry.name);
1308
1153
  const body = await readFile5(filePath, "utf8");
1309
1154
  if (!body.trim()) {
1310
1155
  pushPathFinding(findings, "error", skill, "Reference file must not be empty.", filePath);
@@ -1312,7 +1157,7 @@ async function validateSkillDirectory(skill) {
1312
1157
  }
1313
1158
  }
1314
1159
  if (await exists(evalsDir)) {
1315
- const triggersPath = path9.join(evalsDir, "triggers.json");
1160
+ const triggersPath = path7.join(evalsDir, "triggers.json");
1316
1161
  if (!await exists(triggersPath)) {
1317
1162
  pushPathFinding(findings, "warning", skill, "Skill evals directory should include triggers.json.", evalsDir);
1318
1163
  } else {
@@ -1357,7 +1202,7 @@ async function loadSkillsAtPath(targetPath) {
1357
1202
  if (info.isFile()) {
1358
1203
  return [await loadSkillFile(targetPath)];
1359
1204
  }
1360
- const directSkill = path9.join(targetPath, "SKILL.md");
1205
+ const directSkill = path7.join(targetPath, "SKILL.md");
1361
1206
  try {
1362
1207
  return [await loadSkillFile(directSkill)];
1363
1208
  } catch (error) {
@@ -1371,12 +1216,12 @@ async function loadSkillsAtPath(targetPath) {
1371
1216
  const entries = await readdir2(targetPath, { withFileTypes: true });
1372
1217
  const skills = [];
1373
1218
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
1374
- const full = path9.join(targetPath, entry.name);
1219
+ const full = path7.join(targetPath, entry.name);
1375
1220
  if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
1376
1221
  skills.push(await loadSkillFile(full));
1377
1222
  }
1378
1223
  if (entry.isDirectory()) {
1379
- const skillPath = path9.join(full, "SKILL.md");
1224
+ const skillPath = path7.join(full, "SKILL.md");
1380
1225
  try {
1381
1226
  skills.push(await loadSkillFile(skillPath));
1382
1227
  } catch (error) {
@@ -1401,15 +1246,15 @@ async function inspectSkillPath(targetPath) {
1401
1246
  throw new HarnessError(`Expected exactly one skill at ${targetPath}; found ${skills.length}.`);
1402
1247
  }
1403
1248
  const skill = skills[0];
1404
- const skillDir = path9.dirname(skill.sourcePath);
1249
+ const skillDir = path7.dirname(skill.sourcePath);
1405
1250
  return {
1406
1251
  name: skill.name,
1407
1252
  description: skill.frontmatter.description,
1408
1253
  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")),
1254
+ references: await listFilesIfDir(path7.join(skillDir, "references")),
1255
+ scripts: await listFilesIfDir(path7.join(skillDir, "scripts")),
1256
+ assets: await listFilesIfDir(path7.join(skillDir, "assets")),
1257
+ evals: await listFilesIfDir(path7.join(skillDir, "evals")),
1413
1258
  allowedTools: skill.frontmatter.allowedTools
1414
1259
  };
1415
1260
  }
@@ -1471,13 +1316,13 @@ async function validateSkills(repoRoot, options = {}) {
1471
1316
  }
1472
1317
 
1473
1318
  // src/core/connections/index.ts
1474
- import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
1475
- import path11 from "path";
1319
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
1320
+ import path9 from "path";
1476
1321
  import { stringify as stringifyYaml2 } from "yaml";
1477
1322
 
1478
1323
  // src/core/tools/execute.ts
1479
1324
  import { spawn } from "child_process";
1480
- import path10 from "path";
1325
+ import path8 from "path";
1481
1326
  var ToolExecutionError = class extends Error {
1482
1327
  constructor(message) {
1483
1328
  super(message);
@@ -1565,15 +1410,15 @@ function runProcess(plan, opts) {
1565
1410
  });
1566
1411
  }
1567
1412
  function planScript(repoRoot, scriptRef) {
1568
- const resolved = path10.resolve(repoRoot, scriptRef);
1413
+ const resolved = path8.resolve(repoRoot, scriptRef);
1569
1414
  const projectRoot = projectHarnessDir(repoRoot);
1570
1415
  const userRoot = userHarnessDir();
1571
- const withinProject = resolved === projectRoot || resolved.startsWith(`${projectRoot}${path10.sep}`);
1572
- const withinUser = resolved === userRoot || resolved.startsWith(`${userRoot}${path10.sep}`);
1416
+ const withinProject = resolved === projectRoot || resolved.startsWith(`${projectRoot}${path8.sep}`);
1417
+ const withinUser = resolved === userRoot || resolved.startsWith(`${userRoot}${path8.sep}`);
1573
1418
  if (!withinProject && !withinUser) {
1574
1419
  throw new ToolExecutionError(`Script must live under the harness directory: ${scriptRef}`);
1575
1420
  }
1576
- const interpreter = INTERPRETERS[path10.extname(resolved).toLowerCase()];
1421
+ const interpreter = INTERPRETERS[path8.extname(resolved).toLowerCase()];
1577
1422
  if (interpreter) {
1578
1423
  return { file: interpreter, args: [resolved], shell: false, label: `${interpreter} ${scriptRef}` };
1579
1424
  }
@@ -1650,9 +1495,9 @@ async function createConnection(repoRoot, input2, options = {}) {
1650
1495
  throw new ConnectionCreateError(`Invalid connection definition: ${detail}`);
1651
1496
  }
1652
1497
  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(
1498
+ const filePath = path9.join(dir, `${input2.name}.yaml`);
1499
+ await mkdir3(dir, { recursive: true });
1500
+ await writeFile3(filePath, stringifyYaml2(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
1656
1501
  (error) => {
1657
1502
  if (error.code === "EEXIST") {
1658
1503
  throw new ConnectionCreateError(
@@ -1712,12 +1557,12 @@ async function checkConnections(repoRoot, options = {}) {
1712
1557
  }
1713
1558
 
1714
1559
  // src/core/setup.ts
1715
- import { mkdir as mkdir5, readFile as readFile6, rm, stat as stat3, writeFile as writeFile5 } from "fs/promises";
1560
+ import { mkdir as mkdir4, readFile as readFile6, rm, stat as stat3, writeFile as writeFile4 } from "fs/promises";
1716
1561
  import { homedir } from "os";
1717
- import path13 from "path";
1562
+ import path11 from "path";
1718
1563
 
1719
1564
  // src/core/agent-providers.ts
1720
- import path12 from "path";
1565
+ import path10 from "path";
1721
1566
  var AGENT_PROVIDER_IDS = [
1722
1567
  "antigravity",
1723
1568
  "claude",
@@ -1732,50 +1577,50 @@ var AGENT_PROVIDERS = {
1732
1577
  antigravity: {
1733
1578
  id: "antigravity",
1734
1579
  label: "Antigravity",
1735
- projectSkillDir: path12.join(".agent", "skills"),
1736
- globalSkillDir: path12.join(".gemini", "antigravity", "skills")
1580
+ projectSkillDir: path10.join(".agent", "skills"),
1581
+ globalSkillDir: path10.join(".gemini", "antigravity", "skills")
1737
1582
  },
1738
1583
  claude: {
1739
1584
  id: "claude",
1740
1585
  label: "Claude Code",
1741
- projectSkillDir: path12.join(".claude", "skills"),
1742
- globalSkillDir: path12.join(".claude", "skills")
1586
+ projectSkillDir: path10.join(".claude", "skills"),
1587
+ globalSkillDir: path10.join(".claude", "skills")
1743
1588
  },
1744
1589
  codex: {
1745
1590
  id: "codex",
1746
1591
  label: "Codex",
1747
- projectSkillDir: path12.join(".agents", "skills"),
1748
- globalSkillDir: path12.join(".agents", "skills")
1592
+ projectSkillDir: path10.join(".agents", "skills"),
1593
+ globalSkillDir: path10.join(".agents", "skills")
1749
1594
  },
1750
1595
  cursor: {
1751
1596
  id: "cursor",
1752
1597
  label: "Cursor",
1753
- projectSkillDir: path12.join(".cursor", "skills"),
1754
- globalSkillDir: path12.join(".cursor", "skills")
1598
+ projectSkillDir: path10.join(".cursor", "skills"),
1599
+ globalSkillDir: path10.join(".cursor", "skills")
1755
1600
  },
1756
1601
  gemini: {
1757
1602
  id: "gemini",
1758
1603
  label: "Gemini CLI",
1759
- projectSkillDir: path12.join(".gemini", "skills"),
1760
- globalSkillDir: path12.join(".gemini", "skills")
1604
+ projectSkillDir: path10.join(".gemini", "skills"),
1605
+ globalSkillDir: path10.join(".gemini", "skills")
1761
1606
  },
1762
1607
  copilot: {
1763
1608
  id: "copilot",
1764
1609
  label: "GitHub Copilot",
1765
- projectSkillDir: path12.join(".github", "skills"),
1766
- globalSkillDir: path12.join(".copilot", "skills")
1610
+ projectSkillDir: path10.join(".github", "skills"),
1611
+ globalSkillDir: path10.join(".copilot", "skills")
1767
1612
  },
1768
1613
  opencode: {
1769
1614
  id: "opencode",
1770
1615
  label: "OpenCode",
1771
- projectSkillDir: path12.join(".opencode", "skills"),
1772
- globalSkillDir: path12.join(".config", "opencode", "skills")
1616
+ projectSkillDir: path10.join(".opencode", "skills"),
1617
+ globalSkillDir: path10.join(".config", "opencode", "skills")
1773
1618
  },
1774
1619
  windsurf: {
1775
1620
  id: "windsurf",
1776
1621
  label: "Windsurf",
1777
- projectSkillDir: path12.join(".windsurf", "skills"),
1778
- globalSkillDir: path12.join(".codeium", "windsurf", "skills")
1622
+ projectSkillDir: path10.join(".windsurf", "skills"),
1623
+ globalSkillDir: path10.join(".codeium", "windsurf", "skills")
1779
1624
  }
1780
1625
  };
1781
1626
  var ALIASES = {
@@ -1871,16 +1716,17 @@ function threadrootSkillContent(provider, scope) {
1871
1716
  "## Workflow",
1872
1717
  "",
1873
1718
  "1. If `threadroot --version` works, use `threadroot`. Otherwise use `npx --yes threadroot@latest` for one-off commands.",
1874
- "2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot init` or `npx --yes threadroot@latest init`.",
1875
- "3. Before coding in a Threadroot repo, run `threadroot doctor` and resolve errors. Treat warnings as review items, not automatic blockers.",
1876
- '4. For the current task, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
1877
- "5. Use `threadroot status` to inspect harness state and `threadroot diff` only when compiled adapter outputs are enabled.",
1878
- "6. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools when required.",
1879
- "7. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
1719
+ "2. If `.threadroot/harness.yaml` is missing and the user wants setup, run `threadroot bootstrap --yes` or `npx --yes threadroot@latest bootstrap --yes`.",
1720
+ '3. At the start of a coding session, run `threadroot start "<task>"` to get doctor status, project state, relevant skills, tools, memory, and the command map.',
1721
+ '4. For a narrower slice, run `threadroot context "<task>"` and use the returned skills, rules, tools, memory, and references before doing broad file reads.',
1722
+ "5. Use `threadroot tools list`, `threadroot tools check`, and `threadroot run <tool>` for explicit local capabilities. Confirm risky tools when required.",
1723
+ "6. Do not create provider-specific files unless the user asks. Use `threadroot expose <agent>` when native project skill shims are desired.",
1880
1724
  "",
1881
1725
  "## Useful Commands",
1882
1726
  "",
1883
1727
  "```bash",
1728
+ "threadroot bootstrap --yes",
1729
+ 'threadroot start "<task>"',
1884
1730
  "threadroot doctor",
1885
1731
  "threadroot status",
1886
1732
  'threadroot context "<task>"',
@@ -1928,17 +1774,17 @@ async function fileExists(filePath) {
1928
1774
  }
1929
1775
  }
1930
1776
  function displayPath(home, filePath) {
1931
- const relative = path13.relative(home, filePath);
1932
- return relative && !relative.startsWith("..") ? path13.join("~", relative) : filePath;
1777
+ const relative = path11.relative(home, filePath);
1778
+ return relative && !relative.startsWith("..") ? path11.join("~", relative) : filePath;
1933
1779
  }
1934
1780
  function globalSkillPath(home, provider) {
1935
- return path13.join(home, provider.globalSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
1781
+ return path11.join(home, provider.globalSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
1936
1782
  }
1937
1783
  function codexAgentsPath(home) {
1938
- return path13.join(home, ".codex", "AGENTS.md");
1784
+ return path11.join(home, ".codex", "AGENTS.md");
1939
1785
  }
1940
1786
  function codexConfigPath(home) {
1941
- return path13.join(home, ".codex", "config.toml");
1787
+ return path11.join(home, ".codex", "config.toml");
1942
1788
  }
1943
1789
  function codexAgentsBlock() {
1944
1790
  return [
@@ -1946,18 +1792,22 @@ function codexAgentsBlock() {
1946
1792
  "## Threadroot",
1947
1793
  "",
1948
1794
  "When a repository contains `.threadroot/`, treat it as the source of truth for agent harness context.",
1949
- 'Before coding, prefer `threadroot doctor` and `threadroot context "<task>"` over broad, unfocused file reads.',
1795
+ 'Before coding, prefer `threadroot start "<task>"` over broad, unfocused file reads.',
1796
+ "If `.threadroot/` is missing and the user wants setup, run `threadroot bootstrap --yes`.",
1950
1797
  "Do not create provider-specific project files unless the user asks; use `threadroot expose <agent>` for that.",
1951
1798
  CODEX_AGENTS_END,
1952
1799
  ""
1953
1800
  ].join("\n");
1954
1801
  }
1955
- function codexMcpBlock() {
1802
+ function tomlString(value) {
1803
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1804
+ }
1805
+ function codexMcpBlock(entry) {
1956
1806
  return [
1957
1807
  CODEX_MCP_BEGIN,
1958
1808
  "[mcp_servers.threadroot]",
1959
- 'command = "threadroot"',
1960
- 'args = ["mcp"]',
1809
+ `command = ${tomlString(entry.command)}`,
1810
+ `args = [${entry.args.map(tomlString).join(", ")}]`,
1961
1811
  CODEX_MCP_END,
1962
1812
  ""
1963
1813
  ].join("\n");
@@ -1994,7 +1844,7 @@ async function setupGlobalSkill(home, provider, mode, force) {
1994
1844
  message: "Existing skill is not Threadroot-managed."
1995
1845
  };
1996
1846
  }
1997
- await rm(path13.dirname(filePath), { recursive: true, force: true });
1847
+ await rm(path11.dirname(filePath), { recursive: true, force: true });
1998
1848
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "removed" };
1999
1849
  }
2000
1850
  if (existing === desired) {
@@ -2014,8 +1864,8 @@ async function setupGlobalSkill(home, provider, mode, force) {
2014
1864
  if (mode === "dry-run") {
2015
1865
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2016
1866
  }
2017
- await mkdir5(path13.dirname(filePath), { recursive: true });
2018
- await writeFile5(filePath, desired, "utf8");
1867
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1868
+ await writeFile4(filePath, desired, "utf8");
2019
1869
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2020
1870
  }
2021
1871
  async function setupCodexAgents(home, mode) {
@@ -2036,7 +1886,7 @@ async function setupCodexAgents(home, mode) {
2036
1886
  if (!hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END)) {
2037
1887
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "missing" };
2038
1888
  }
2039
- await writeFile5(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
1889
+ await writeFile4(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
2040
1890
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "removed" };
2041
1891
  }
2042
1892
  if (existing === desired) {
@@ -2046,11 +1896,11 @@ async function setupCodexAgents(home, mode) {
2046
1896
  if (mode === "dry-run") {
2047
1897
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2048
1898
  }
2049
- await mkdir5(path13.dirname(filePath), { recursive: true });
2050
- await writeFile5(filePath, desired, "utf8");
1899
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1900
+ await writeFile4(filePath, desired, "utf8");
2051
1901
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2052
1902
  }
2053
- async function setupCodexMcp(home, mode) {
1903
+ async function setupCodexMcp(home, mode, entry) {
2054
1904
  const filePath = codexConfigPath(home);
2055
1905
  const shown = displayPath(home, filePath);
2056
1906
  const existing = await readMaybe(filePath) ?? "";
@@ -2064,7 +1914,7 @@ async function setupCodexMcp(home, mode) {
2064
1914
  message: "Existing unmanaged [mcp_servers.threadroot] table found. Leaving it untouched."
2065
1915
  };
2066
1916
  }
2067
- const desired = upsertManagedBlock(existing, codexMcpBlock(), CODEX_MCP_BEGIN, CODEX_MCP_END);
1917
+ const desired = upsertManagedBlock(existing, codexMcpBlock(entry), CODEX_MCP_BEGIN, CODEX_MCP_END);
2068
1918
  if (mode === "check") {
2069
1919
  return {
2070
1920
  kind: "codex-mcp",
@@ -2078,7 +1928,7 @@ async function setupCodexMcp(home, mode) {
2078
1928
  if (!hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
2079
1929
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "missing" };
2080
1930
  }
2081
- await writeFile5(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
1931
+ await writeFile4(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
2082
1932
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "removed" };
2083
1933
  }
2084
1934
  if (existing === desired) {
@@ -2088,8 +1938,8 @@ async function setupCodexMcp(home, mode) {
2088
1938
  if (mode === "dry-run") {
2089
1939
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2090
1940
  }
2091
- await mkdir5(path13.dirname(filePath), { recursive: true });
2092
- await writeFile5(filePath, desired, "utf8");
1941
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1942
+ await writeFile4(filePath, desired, "utf8");
2093
1943
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2094
1944
  }
2095
1945
  async function setupGlobal(options = {}) {
@@ -2103,7 +1953,7 @@ async function setupGlobal(options = {}) {
2103
1953
  if (providerIds.includes("codex")) {
2104
1954
  entries.push(await setupCodexAgents(home, mode));
2105
1955
  if (options.mcp) {
2106
- entries.push(await setupCodexMcp(home, mode));
1956
+ entries.push(await setupCodexMcp(home, mode, options.mcpEntry ?? { command: "threadroot", args: ["mcp"] }));
2107
1957
  }
2108
1958
  }
2109
1959
  return { entries };
@@ -2235,8 +2085,8 @@ function inputEnv(values) {
2235
2085
  }
2236
2086
 
2237
2087
  // src/core/tools/create.ts
2238
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
2239
- import path14 from "path";
2088
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
2089
+ import path12 from "path";
2240
2090
  import { stringify as stringifyYaml3 } from "yaml";
2241
2091
  var ToolCreateError = class extends Error {
2242
2092
  constructor(message) {
@@ -2253,7 +2103,7 @@ function assertSafeName(name) {
2253
2103
  }
2254
2104
  }
2255
2105
  function assertSafeScript(script) {
2256
- if (path14.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
2106
+ if (path12.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
2257
2107
  throw new ToolCreateError(`Script path must be inside the harness directory: ${script}`);
2258
2108
  }
2259
2109
  }
@@ -2282,9 +2132,9 @@ async function createTool(repoRoot, input2, options) {
2282
2132
  throw new ToolCreateError(`Invalid tool definition: ${detail}`);
2283
2133
  }
2284
2134
  const dir = scope === "project" ? projectObjectDir(repoRoot, "tools") : userObjectDir("tools", options.home);
2285
- const filePath = path14.join(dir, `${input2.name}.yaml`);
2286
- await mkdir6(dir, { recursive: true });
2287
- await writeFile6(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
2135
+ const filePath = path12.join(dir, `${input2.name}.yaml`);
2136
+ await mkdir5(dir, { recursive: true });
2137
+ await writeFile5(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
2288
2138
  (error) => {
2289
2139
  if (error.code === "EEXIST") {
2290
2140
  throw new ToolCreateError(`Tool \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`);
@@ -2297,7 +2147,7 @@ async function createTool(repoRoot, input2, options) {
2297
2147
 
2298
2148
  // src/core/tools/catalog.ts
2299
2149
  import { readFile as readFile7, stat as stat4 } from "fs/promises";
2300
- import path15 from "path";
2150
+ import path13 from "path";
2301
2151
  var NAME_RE3 = /^[a-z0-9][a-z0-9-]*$/;
2302
2152
  var DESTRUCTIVE_RE = /\b(migrate|deploy|publish|release|prune|reset|destroy|drop|delete|rm|push|seed|wipe)\b/i;
2303
2153
  function looksDestructive(name, command) {
@@ -2316,13 +2166,13 @@ async function exists2(file) {
2316
2166
  }
2317
2167
  }
2318
2168
  async function detectPackageManager(repoRoot) {
2319
- if (await exists2(path15.join(repoRoot, "pnpm-lock.yaml"))) {
2169
+ if (await exists2(path13.join(repoRoot, "pnpm-lock.yaml"))) {
2320
2170
  return "pnpm";
2321
2171
  }
2322
- if (await exists2(path15.join(repoRoot, "yarn.lock"))) {
2172
+ if (await exists2(path13.join(repoRoot, "yarn.lock"))) {
2323
2173
  return "yarn";
2324
2174
  }
2325
- if (await exists2(path15.join(repoRoot, "bun.lockb"))) {
2175
+ if (await exists2(path13.join(repoRoot, "bun.lockb"))) {
2326
2176
  return "bun";
2327
2177
  }
2328
2178
  return "npm";
@@ -2343,7 +2193,7 @@ function orderScripts(names) {
2343
2193
  });
2344
2194
  }
2345
2195
  async function fromPackageJson(repoRoot) {
2346
- const file = path15.join(repoRoot, "package.json");
2196
+ const file = path13.join(repoRoot, "package.json");
2347
2197
  let raw;
2348
2198
  try {
2349
2199
  raw = await readFile7(file, "utf8");
@@ -2380,7 +2230,7 @@ async function fromPackageJson(repoRoot) {
2380
2230
  async function fromTargets(repoRoot, fileName, runner, source) {
2381
2231
  let raw;
2382
2232
  try {
2383
- raw = await readFile7(path15.join(repoRoot, fileName), "utf8");
2233
+ raw = await readFile7(path13.join(repoRoot, fileName), "utf8");
2384
2234
  } catch {
2385
2235
  return [];
2386
2236
  }
@@ -2530,17 +2380,387 @@ async function checkToolHealth(repoRoot, tool) {
2530
2380
  if (!tool.manifest.healthcheck) {
2531
2381
  return { status: "skipped", tool: tool.name, message: "No healthcheck configured." };
2532
2382
  }
2533
- const result = await executeShell(tool.manifest.healthcheck.run, { cwd: repoRoot, timeoutMs: 3e4 });
2534
- const expected = tool.manifest.healthcheck.expectExitCode;
2535
- if (result.exitCode !== expected) {
2536
- return {
2537
- status: "error",
2538
- tool: tool.name,
2539
- message: `Healthcheck exited ${result.exitCode}; expected ${expected}.`,
2540
- result
2541
- };
2383
+ const result = await executeShell(tool.manifest.healthcheck.run, { cwd: repoRoot, timeoutMs: 3e4 });
2384
+ const expected = tool.manifest.healthcheck.expectExitCode;
2385
+ if (result.exitCode !== expected) {
2386
+ return {
2387
+ status: "error",
2388
+ tool: tool.name,
2389
+ message: `Healthcheck exited ${result.exitCode}; expected ${expected}.`,
2390
+ result
2391
+ };
2392
+ }
2393
+ return { status: "ok", tool: tool.name, result };
2394
+ }
2395
+
2396
+ // src/core/mcp-check.ts
2397
+ import { spawn as spawn2 } from "child_process";
2398
+ import { access, mkdtemp, readFile as readFile8, rm as rm2, writeFile as writeFile6 } from "fs/promises";
2399
+ import { constants, realpathSync } from "fs";
2400
+ import { homedir as homedir2, tmpdir } from "os";
2401
+ import path14 from "path";
2402
+ var REQUIRED_MCP_TOOLS = [
2403
+ "context",
2404
+ "skills_list",
2405
+ "skills_get",
2406
+ "tools_list",
2407
+ "tools_check",
2408
+ "tools_run",
2409
+ "tools_create",
2410
+ "tools_detect",
2411
+ "connections_list",
2412
+ "connections_check",
2413
+ "memory_read",
2414
+ "memory_append",
2415
+ "status",
2416
+ "doctor"
2417
+ ];
2418
+ function codexConfigPath2(home = homedir2()) {
2419
+ return path14.join(home, ".codex", "config.toml");
2420
+ }
2421
+ function mcpEntryForCurrentProcess() {
2422
+ const scriptPath = currentScriptPath();
2423
+ if (scriptPath && path14.basename(scriptPath) === "index.js" && scriptPath.includes(`${path14.sep}dist${path14.sep}`)) {
2424
+ return { command: process.execPath, args: [scriptPath, "mcp"] };
2425
+ }
2426
+ return { command: "threadroot", args: ["mcp"] };
2427
+ }
2428
+ function currentScriptPath() {
2429
+ const scriptPath = process.argv[1];
2430
+ if (!scriptPath) {
2431
+ return void 0;
2432
+ }
2433
+ try {
2434
+ return realpathSync(scriptPath);
2435
+ } catch {
2436
+ return scriptPath;
2437
+ }
2438
+ }
2439
+ async function readCodexThreadrootMcpEntry(home = homedir2()) {
2440
+ let raw;
2441
+ try {
2442
+ raw = await readFile8(codexConfigPath2(home), "utf8");
2443
+ } catch (error) {
2444
+ if (error.code === "ENOENT") {
2445
+ return void 0;
2446
+ }
2447
+ throw error;
2448
+ }
2449
+ const table = raw.match(/(?:^|\n)\[mcp_servers\.threadroot\]\s*\n(?<body>[\s\S]*?)(?=\n\[|$)/);
2450
+ if (!table?.groups?.body) {
2451
+ return void 0;
2452
+ }
2453
+ const command = matchTomlString(table.groups.body, "command");
2454
+ const args = matchTomlArray(table.groups.body, "args");
2455
+ if (!command || !args) {
2456
+ return void 0;
2457
+ }
2458
+ return { command, args };
2459
+ }
2460
+ function matchTomlString(body, key) {
2461
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2462
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*"(?<value>(?:[^"\\\\]|\\\\.)*)"\\s*$`, "m"));
2463
+ return match?.groups?.value?.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
2464
+ }
2465
+ function matchTomlArray(body, key) {
2466
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2467
+ const match = body.match(new RegExp(`^${escaped}\\s*=\\s*\\[(?<value>.*)\\]\\s*$`, "m"));
2468
+ if (!match?.groups?.value) {
2469
+ return void 0;
2470
+ }
2471
+ const values = [];
2472
+ const pattern = /"((?:[^"\\]|\\.)*)"/g;
2473
+ let value;
2474
+ while (value = pattern.exec(match.groups.value)) {
2475
+ values.push(value[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
2476
+ }
2477
+ return values;
2478
+ }
2479
+ async function commandExists2(command) {
2480
+ if (path14.isAbsolute(command) || command.includes(path14.sep)) {
2481
+ try {
2482
+ await access(command, constants.X_OK);
2483
+ return true;
2484
+ } catch {
2485
+ return false;
2486
+ }
2487
+ }
2488
+ const paths = (process.env.PATH ?? "").split(path14.delimiter).filter(Boolean);
2489
+ for (const dir of paths) {
2490
+ try {
2491
+ await access(path14.join(dir, command), constants.X_OK);
2492
+ return true;
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ return false;
2497
+ }
2498
+ async function checkCodexMcp(input2) {
2499
+ const configPath = codexConfigPath2(input2.home);
2500
+ const entry = await readCodexThreadrootMcpEntry(input2.home);
2501
+ if (!entry) {
2502
+ return {
2503
+ status: "warning",
2504
+ configPath,
2505
+ tools: [],
2506
+ messages: ["No Codex Threadroot MCP config found."]
2507
+ };
2508
+ }
2509
+ if (!await commandExists2(entry.command)) {
2510
+ return {
2511
+ status: "error",
2512
+ configPath,
2513
+ entry,
2514
+ tools: [],
2515
+ messages: [`MCP command is not executable or not on PATH: ${entry.command}`]
2516
+ };
2517
+ }
2518
+ try {
2519
+ const handshake = await runMcpHandshake(entry, input2.repoRoot, input2.timeoutMs ?? 4e3);
2520
+ const toolNames = handshake.tools;
2521
+ const missing = REQUIRED_MCP_TOOLS.filter((tool) => !toolNames.includes(tool));
2522
+ if (missing.length > 0) {
2523
+ return {
2524
+ status: "error",
2525
+ configPath,
2526
+ entry,
2527
+ serverInfo: handshake.serverInfo,
2528
+ tools: toolNames,
2529
+ messages: [`MCP server is missing required tool(s): ${missing.join(", ")}`]
2530
+ };
2531
+ }
2532
+ return {
2533
+ status: "ok",
2534
+ configPath,
2535
+ entry,
2536
+ serverInfo: handshake.serverInfo,
2537
+ tools: toolNames,
2538
+ messages: ["MCP server initialized and returned the expected Threadroot tools."]
2539
+ };
2540
+ } catch (error) {
2541
+ return {
2542
+ status: "error",
2543
+ configPath,
2544
+ entry,
2545
+ tools: [],
2546
+ messages: [`MCP handshake failed: ${error instanceof Error ? error.message : String(error)}`]
2547
+ };
2548
+ }
2549
+ }
2550
+ function runMcpHandshake(entry, repoRoot, timeoutMs) {
2551
+ if (process.platform !== "win32") {
2552
+ return runOneShotMcpHandshake(entry, repoRoot, timeoutMs);
2553
+ }
2554
+ return new Promise((resolve, reject) => {
2555
+ const child = spawn2(entry.command, entry.args, {
2556
+ cwd: repoRoot,
2557
+ stdio: ["pipe", "pipe", "pipe"],
2558
+ env: process.env
2559
+ });
2560
+ let settled = false;
2561
+ const finish = (result) => {
2562
+ if (settled) return;
2563
+ settled = true;
2564
+ clearTimeout(timer);
2565
+ child.kill();
2566
+ resolve(result);
2567
+ };
2568
+ const fail = (error) => {
2569
+ if (settled) return;
2570
+ settled = true;
2571
+ clearTimeout(timer);
2572
+ child.kill();
2573
+ reject(error);
2574
+ };
2575
+ const timer = setTimeout(() => {
2576
+ fail(new Error(`Timed out after ${timeoutMs}ms`));
2577
+ }, timeoutMs);
2578
+ let stdout = "";
2579
+ let stderr = "";
2580
+ let initialized = false;
2581
+ let serverInfo;
2582
+ child.stdout.setEncoding("utf8");
2583
+ child.stderr.setEncoding("utf8");
2584
+ child.stdout.on("data", (chunk) => {
2585
+ stdout += chunk;
2586
+ const lines = stdout.split("\n");
2587
+ stdout = lines.pop() ?? "";
2588
+ for (const line of lines) {
2589
+ if (!line.trim()) {
2590
+ continue;
2591
+ }
2592
+ let message;
2593
+ try {
2594
+ message = JSON.parse(line);
2595
+ } catch (error) {
2596
+ fail(new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`));
2597
+ return;
2598
+ }
2599
+ if (message.error) {
2600
+ fail(new Error(message.error.message ?? "MCP server returned an error"));
2601
+ return;
2602
+ }
2603
+ if (message.id === 1) {
2604
+ initialized = true;
2605
+ serverInfo = message.result?.serverInfo;
2606
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}
2607
+ `);
2608
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" })}
2609
+ `);
2610
+ }
2611
+ if (message.id === 2) {
2612
+ finish({
2613
+ serverInfo,
2614
+ tools: (message.result?.tools ?? []).map((tool) => tool.name)
2615
+ });
2616
+ }
2617
+ }
2618
+ });
2619
+ child.stderr.on("data", (chunk) => {
2620
+ stderr += chunk;
2621
+ });
2622
+ child.on("error", (error) => {
2623
+ fail(error);
2624
+ });
2625
+ child.on("close", (code) => {
2626
+ if (!settled && !initialized && code !== null) {
2627
+ fail(new Error(`Server exited before initialize completed (exit ${code})${stderr ? `: ${stderr}` : ""}`));
2628
+ }
2629
+ });
2630
+ child.stdin.write(
2631
+ `${JSON.stringify({
2632
+ jsonrpc: "2.0",
2633
+ id: 1,
2634
+ method: "initialize",
2635
+ params: {
2636
+ protocolVersion: "2024-11-05",
2637
+ capabilities: {},
2638
+ clientInfo: { name: "threadroot-check", version: "0.1.3" }
2639
+ }
2640
+ })}
2641
+ `
2642
+ );
2643
+ });
2644
+ }
2645
+ async function runOneShotMcpHandshake(entry, repoRoot, timeoutMs) {
2646
+ const tempDir = await mkdtemp(path14.join(tmpdir(), "threadroot-mcp-check-"));
2647
+ const inputPath = path14.join(tempDir, "input.jsonl");
2648
+ const stdoutPath = path14.join(tempDir, "stdout.jsonl");
2649
+ const stderrPath = path14.join(tempDir, "stderr.txt");
2650
+ try {
2651
+ await writeFile6(
2652
+ inputPath,
2653
+ [
2654
+ JSON.stringify({
2655
+ jsonrpc: "2.0",
2656
+ id: 1,
2657
+ method: "initialize",
2658
+ params: {
2659
+ protocolVersion: "2024-11-05",
2660
+ capabilities: {},
2661
+ clientInfo: { name: "threadroot-check", version: "0.1.3" }
2662
+ }
2663
+ }),
2664
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
2665
+ JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list" }),
2666
+ ""
2667
+ ].join("\n"),
2668
+ "utf8"
2669
+ );
2670
+ const commandLine = [
2671
+ shellQuote3(entry.command),
2672
+ ...entry.args.map(shellQuote3),
2673
+ "<",
2674
+ shellQuote3(inputPath),
2675
+ ">",
2676
+ shellQuote3(stdoutPath),
2677
+ "2>",
2678
+ shellQuote3(stderrPath)
2679
+ ].join(" ");
2680
+ await runShell(commandLine, repoRoot, timeoutMs);
2681
+ const [stdout, stderr] = await Promise.all([readFile8(stdoutPath, "utf8"), readOptional(stderrPath)]);
2682
+ if (stderr.trim()) {
2683
+ throw new Error(stderr.trim());
2684
+ }
2685
+ return parseHandshakeOutput(stdout);
2686
+ } finally {
2687
+ await rm2(tempDir, { recursive: true, force: true });
2688
+ }
2689
+ }
2690
+ function runShell(commandLine, repoRoot, timeoutMs) {
2691
+ return new Promise((resolve, reject) => {
2692
+ const child = spawn2("sh", ["-c", commandLine], {
2693
+ cwd: repoRoot,
2694
+ stdio: "ignore",
2695
+ env: process.env
2696
+ });
2697
+ let settled = false;
2698
+ const fail = (error) => {
2699
+ if (settled) return;
2700
+ settled = true;
2701
+ clearTimeout(timer);
2702
+ child.kill();
2703
+ reject(error);
2704
+ };
2705
+ const timer = setTimeout(() => fail(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
2706
+ child.on("error", fail);
2707
+ child.on("close", async (code) => {
2708
+ if (settled) return;
2709
+ settled = true;
2710
+ clearTimeout(timer);
2711
+ if (code === 0) {
2712
+ resolve();
2713
+ return;
2714
+ }
2715
+ reject(new Error(`Server exited with status ${code ?? "unknown"}`));
2716
+ });
2717
+ });
2718
+ }
2719
+ function parseHandshakeOutput(stdout) {
2720
+ let initialized = false;
2721
+ let serverInfo;
2722
+ let tools2;
2723
+ for (const line of stdout.split("\n")) {
2724
+ if (!line.trim()) {
2725
+ continue;
2726
+ }
2727
+ let message;
2728
+ try {
2729
+ message = JSON.parse(line);
2730
+ } catch (error) {
2731
+ throw new Error(`Invalid JSON-RPC response: ${error instanceof Error ? error.message : String(error)}`);
2732
+ }
2733
+ if (message.error) {
2734
+ throw new Error(message.error.message ?? "MCP server returned an error");
2735
+ }
2736
+ if (message.id === 1) {
2737
+ initialized = true;
2738
+ serverInfo = message.result?.serverInfo;
2739
+ }
2740
+ if (message.id === 2) {
2741
+ tools2 = (message.result?.tools ?? []).map((tool) => tool.name);
2742
+ }
2743
+ }
2744
+ if (!initialized) {
2745
+ throw new Error("Server did not return an initialize response");
2746
+ }
2747
+ if (!tools2) {
2748
+ throw new Error("Server did not return a tools/list response");
2749
+ }
2750
+ return { serverInfo, tools: tools2 };
2751
+ }
2752
+ async function readOptional(filePath) {
2753
+ try {
2754
+ return await readFile8(filePath, "utf8");
2755
+ } catch (error) {
2756
+ if (error.code === "ENOENT") {
2757
+ return "";
2758
+ }
2759
+ throw error;
2542
2760
  }
2543
- return { status: "ok", tool: tool.name, result };
2761
+ }
2762
+ function shellQuote3(value) {
2763
+ return `'${value.replace(/'/g, "'\\''")}'`;
2544
2764
  }
2545
2765
 
2546
2766
  // src/core/doctor.ts
@@ -2558,9 +2778,23 @@ async function exists3(filePath) {
2558
2778
  function finding2(severity, code, message, pathValue) {
2559
2779
  return pathValue ? { severity, code, message, path: pathValue } : { severity, code, message };
2560
2780
  }
2561
- async function mcpConfigHints(repoRoot) {
2781
+ async function mcpConfigHints(repoRoot, home) {
2782
+ const codexMcp = await checkCodexMcp({ repoRoot, home, timeoutMs: 2500 });
2783
+ if (codexMcp.status === "ok") {
2784
+ return [];
2785
+ }
2786
+ if (codexMcp.status === "error") {
2787
+ return [
2788
+ finding2(
2789
+ "warning",
2790
+ "codex_mcp_unhealthy",
2791
+ `Codex Threadroot MCP is configured but failed verification: ${codexMcp.messages.join(" ")}`,
2792
+ codexMcp.configPath
2793
+ )
2794
+ ];
2795
+ }
2562
2796
  const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2563
- const present = await Promise.all(configs.map((config) => exists3(path16.join(repoRoot, config))));
2797
+ const present = await Promise.all(configs.map((config) => exists3(path15.join(repoRoot, config))));
2564
2798
  if (present.some(Boolean)) {
2565
2799
  return [];
2566
2800
  }
@@ -2580,7 +2814,7 @@ async function globalSetupHints(home) {
2580
2814
  finding2(
2581
2815
  "info",
2582
2816
  "global_setup_missing",
2583
- "Codex global Threadroot setup was not detected. Run `threadroot setup --global --agent codex` for one-time machine setup."
2817
+ "Codex global Threadroot setup was not detected. Run `threadroot bootstrap --yes --agent codex` for one-time machine setup."
2584
2818
  )
2585
2819
  ];
2586
2820
  }
@@ -2734,7 +2968,7 @@ async function doctor(repoRoot, options = {}) {
2734
2968
  )
2735
2969
  );
2736
2970
  }
2737
- const scriptsDir = path16.join(path16.dirname(skill.sourcePath), "scripts");
2971
+ const scriptsDir = path15.join(path15.dirname(skill.sourcePath), "scripts");
2738
2972
  if (await exists3(scriptsDir)) {
2739
2973
  findings.push(
2740
2974
  finding2(
@@ -2747,7 +2981,7 @@ async function doctor(repoRoot, options = {}) {
2747
2981
  }
2748
2982
  }
2749
2983
  findings.push(...await globalSetupHints(options.home));
2750
- findings.push(...await mcpConfigHints(repoRoot));
2984
+ findings.push(...await mcpConfigHints(repoRoot, options.home));
2751
2985
  return summarize(findings);
2752
2986
  }
2753
2987
  function summarize(findings) {
@@ -2757,38 +2991,12 @@ function summarize(findings) {
2757
2991
  return { ok: errors === 0, findings, summary: { errors, warnings, info } };
2758
2992
  }
2759
2993
 
2760
- // src/commands/doctor.ts
2761
- async function runDoctor(repoRoot) {
2762
- const report = await doctor(repoRoot);
2763
- const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
2764
- const hints = report.findings.filter((finding3) => finding3.severity === "info");
2765
- if (actionable.length === 0) {
2766
- console.log("Threadroot doctor: clean");
2767
- for (const finding3 of hints) {
2768
- const suffix = finding3.path ? ` (${finding3.path})` : "";
2769
- console.log(`- hint ${finding3.code}: ${finding3.message}${suffix}`);
2770
- }
2771
- return;
2772
- }
2773
- console.log(
2774
- `Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`
2775
- );
2776
- for (const finding3 of report.findings) {
2777
- const label = finding3.severity === "error" ? "error" : finding3.severity === "warning" ? "warning" : "hint";
2778
- const suffix = finding3.path ? ` (${finding3.path})` : "";
2779
- console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
2780
- }
2781
- if (!report.ok) {
2782
- process.exitCode = 1;
2783
- }
2784
- }
2785
-
2786
2994
  // src/core/expose.ts
2787
- import { mkdir as mkdir7, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile7 } from "fs/promises";
2788
- import path17 from "path";
2995
+ import { mkdir as mkdir6, readFile as readFile9, rm as rm3, stat as stat6, writeFile as writeFile7 } from "fs/promises";
2996
+ import path16 from "path";
2789
2997
  async function readMaybe2(filePath) {
2790
2998
  try {
2791
- return await readFile8(filePath, "utf8");
2999
+ return await readFile9(filePath, "utf8");
2792
3000
  } catch (error) {
2793
3001
  if (error.code === "ENOENT") {
2794
3002
  return void 0;
@@ -2797,10 +3005,10 @@ async function readMaybe2(filePath) {
2797
3005
  }
2798
3006
  }
2799
3007
  function projectSkillPath(repoRoot, provider) {
2800
- return path17.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3008
+ return path16.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2801
3009
  }
2802
3010
  function relSkillPath(provider) {
2803
- return path17.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
3011
+ return path16.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2804
3012
  }
2805
3013
  async function exposeOne(repoRoot, provider, mode, force) {
2806
3014
  const relativePath = relSkillPath(provider);
@@ -2832,7 +3040,7 @@ async function exposeOne(repoRoot, provider, mode, force) {
2832
3040
  message: "Existing skill is not Threadroot-managed."
2833
3041
  };
2834
3042
  }
2835
- await rm2(path17.dirname(absolutePath), { recursive: true, force: true });
3043
+ await rm3(path16.dirname(absolutePath), { recursive: true, force: true });
2836
3044
  return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
2837
3045
  }
2838
3046
  if (existing === desired) {
@@ -2851,7 +3059,7 @@ async function exposeOne(repoRoot, provider, mode, force) {
2851
3059
  if (mode === "dry-run") {
2852
3060
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2853
3061
  }
2854
- await mkdir7(path17.dirname(absolutePath), { recursive: true });
3062
+ await mkdir6(path16.dirname(absolutePath), { recursive: true });
2855
3063
  await writeFile7(absolutePath, desired, "utf8");
2856
3064
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2857
3065
  }
@@ -2865,34 +3073,34 @@ async function exposeProject(repoRoot, options = {}) {
2865
3073
  return { entries };
2866
3074
  }
2867
3075
 
2868
- // src/commands/expose.ts
2869
- function modeFromOptions(options) {
2870
- if (options.undo) return "undo";
2871
- if (options.check) return "check";
2872
- if (options.dryRun) return "dry-run";
2873
- return "write";
2874
- }
2875
- async function runExpose(repoRoot, agent, options) {
2876
- const mode = modeFromOptions(options);
2877
- const result = await exposeProject(repoRoot, {
2878
- agents: agent,
2879
- mode,
2880
- force: options.force
2881
- });
2882
- const verb = mode === "dry-run" ? "Project exposure plan" : mode === "check" ? "Project exposure check" : mode === "undo" ? "Removed project exposure" : "Exposed Threadroot project skills";
2883
- console.log(`${verb}:`);
2884
- for (const entry of result.entries) {
2885
- const suffix = entry.message ? ` - ${entry.message}` : "";
2886
- console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
2887
- }
2888
- }
2889
-
2890
3076
  // src/core/init/index.ts
2891
- import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
3077
+ import { mkdir as mkdir9, stat as stat8, writeFile as writeFile9 } from "fs/promises";
2892
3078
  import path22 from "path";
2893
3079
 
3080
+ // src/core/compile/write.ts
3081
+ import { mkdir as mkdir7, writeFile as writeFile8 } from "fs/promises";
3082
+ import path17 from "path";
3083
+ async function writeCompiled(repoRoot, files) {
3084
+ await Promise.all(
3085
+ files.map(async (file) => {
3086
+ const absolute = path17.join(repoRoot, file.path);
3087
+ await mkdir7(path17.dirname(absolute), { recursive: true });
3088
+ await writeFile8(absolute, file.content, "utf8");
3089
+ })
3090
+ );
3091
+ return files.map((file) => file.path);
3092
+ }
3093
+ async function runCompile(repoRoot, options = {}) {
3094
+ const resolved = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
3095
+ const harness = options.adapter ? { ...resolved, manifest: { ...resolved.manifest, adapters: [options.adapter] } } : resolved;
3096
+ const files = await compile(repoRoot, harness);
3097
+ const drift = await detectDrift(repoRoot, files);
3098
+ const written = await writeCompiled(repoRoot, files);
3099
+ return { written, drift };
3100
+ }
3101
+
2894
3102
  // src/core/scan/package.ts
2895
- import fs2 from "fs/promises";
3103
+ import fs from "fs/promises";
2896
3104
  import path18 from "path";
2897
3105
 
2898
3106
  // src/core/scan/rules.ts
@@ -2911,7 +3119,7 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
2911
3119
  // src/core/scan/package.ts
2912
3120
  async function readJson(repoRoot, relativePath) {
2913
3121
  try {
2914
- return JSON.parse(await fs2.readFile(path18.join(repoRoot, relativePath), "utf8"));
3122
+ return JSON.parse(await fs.readFile(path18.join(repoRoot, relativePath), "utf8"));
2915
3123
  } catch {
2916
3124
  return void 0;
2917
3125
  }
@@ -2941,7 +3149,7 @@ function inferProfile(files, packageJson) {
2941
3149
  }
2942
3150
 
2943
3151
  // src/core/scan/walk.ts
2944
- import fs3 from "fs/promises";
3152
+ import fs2 from "fs/promises";
2945
3153
  import path19 from "path";
2946
3154
  function toPosix(relativePath) {
2947
3155
  return relativePath.split(path19.sep).join("/");
@@ -2949,7 +3157,7 @@ function toPosix(relativePath) {
2949
3157
  async function walkRepo(repoRoot, directory = repoRoot) {
2950
3158
  let entries;
2951
3159
  try {
2952
- entries = await fs3.readdir(directory, { withFileTypes: true });
3160
+ entries = await fs2.readdir(directory, { withFileTypes: true });
2953
3161
  } catch {
2954
3162
  return [];
2955
3163
  }
@@ -2963,363 +3171,835 @@ async function walkRepo(repoRoot, directory = repoRoot) {
2963
3171
  }
2964
3172
  continue;
2965
3173
  }
2966
- if (entry.isFile()) {
2967
- files.push(relativePath);
3174
+ if (entry.isFile()) {
3175
+ files.push(relativePath);
3176
+ }
3177
+ }
3178
+ return files;
3179
+ }
3180
+
3181
+ // src/core/init/index.ts
3182
+ import { stringify as stringifyYaml4 } from "yaml";
3183
+
3184
+ // src/core/init/builtins.ts
3185
+ import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
3186
+ import path20 from "path";
3187
+ import { fileURLToPath } from "url";
3188
+ var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
3189
+ var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
3190
+ var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
3191
+ var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
3192
+ var SKILL_PACK_CANDIDATES = [
3193
+ path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
3194
+ path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
3195
+ path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
3196
+ ];
3197
+ var PROJECT_MEMORY_TEMPLATE = [
3198
+ "# Project",
3199
+ "",
3200
+ "<!-- Stable, rarely-changing facts about this project. Keep it short. -->",
3201
+ "",
3202
+ "- What it is:",
3203
+ "- Key technologies:",
3204
+ "- How to run it:"
3205
+ ].join("\n");
3206
+ async function exists4(target) {
3207
+ try {
3208
+ const info = await stat7(target);
3209
+ return info.isDirectory();
3210
+ } catch {
3211
+ return false;
3212
+ }
3213
+ }
3214
+ async function bundledSkillsDir() {
3215
+ for (const candidate of SKILL_PACK_CANDIDATES) {
3216
+ if (await exists4(candidate)) {
3217
+ return candidate;
3218
+ }
3219
+ }
3220
+ throw new Error("Bundled Threadroot skills were not found in this package.");
3221
+ }
3222
+ async function writeBuiltinSkills(repoRoot) {
3223
+ const sourceDir = await bundledSkillsDir();
3224
+ const targetDir = projectObjectDir(repoRoot, "skills");
3225
+ await mkdir8(targetDir, { recursive: true });
3226
+ const written = [];
3227
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
3228
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3229
+ if (!entry.isDirectory()) {
3230
+ continue;
3231
+ }
3232
+ const sourceSkill = path20.join(sourceDir, entry.name);
3233
+ const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
3234
+ if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
3235
+ continue;
3236
+ }
3237
+ const targetSkill = path20.join(targetDir, entry.name);
3238
+ const targetSkillFile = path20.join(targetSkill, "SKILL.md");
3239
+ try {
3240
+ await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
3241
+ written.push(targetSkillFile);
3242
+ } catch (error) {
3243
+ if (error.code !== "ERR_FS_CP_EEXIST" && error.code !== "EEXIST") {
3244
+ throw error;
3245
+ }
3246
+ }
3247
+ }
3248
+ return written;
3249
+ }
3250
+
3251
+ // src/core/init/import.ts
3252
+ import { readFile as readFile10, readdir as readdir4 } from "fs/promises";
3253
+ import path21 from "path";
3254
+ var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
3255
+ var CURSOR_RULES_DIR = ".cursor/rules";
3256
+ var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
3257
+ async function readIfExists2(filePath) {
3258
+ try {
3259
+ return await readFile10(filePath, "utf8");
3260
+ } catch (error) {
3261
+ if (error.code === "ENOENT") {
3262
+ return void 0;
3263
+ }
3264
+ throw error;
3265
+ }
3266
+ }
3267
+ function normalize(text) {
3268
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
3269
+ }
3270
+ function splitSections(markdown) {
3271
+ const sections = [];
3272
+ let heading = "";
3273
+ let buffer = [];
3274
+ const flush = () => {
3275
+ const text = buffer.join("\n").trim();
3276
+ if (text) {
3277
+ sections.push({ heading, text });
3278
+ }
3279
+ };
3280
+ for (const line of markdown.split(/\r?\n/)) {
3281
+ if (/^#{1,6}\s/.test(line)) {
3282
+ flush();
3283
+ heading = normalize(line.replace(/^#+\s*/, ""));
3284
+ buffer = [line];
3285
+ } else {
3286
+ buffer.push(line);
3287
+ }
3288
+ }
3289
+ flush();
3290
+ return sections;
3291
+ }
3292
+ function novelSections(canonical, other) {
3293
+ const haystack = normalize(canonical);
3294
+ const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
3295
+ return splitSections(other).filter((section) => {
3296
+ if (section.heading && seenHeadings.has(section.heading)) {
3297
+ return false;
3298
+ }
3299
+ return !haystack.includes(normalize(section.text));
3300
+ });
3301
+ }
3302
+ async function listCursorRules(repoRoot) {
3303
+ const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
3304
+ let entries;
3305
+ try {
3306
+ entries = await readdir4(dir);
3307
+ } catch (error) {
3308
+ if (error.code === "ENOENT") {
3309
+ return [];
3310
+ }
3311
+ throw error;
3312
+ }
3313
+ const files = entries.filter((name) => name.endsWith(".mdc")).sort();
3314
+ return Promise.all(
3315
+ files.map(async (name) => ({
3316
+ file: `${CURSOR_RULES_DIR}/${name}`,
3317
+ content: await readFile10(path21.join(dir, name), "utf8")
3318
+ }))
3319
+ );
3320
+ }
3321
+ function globsToApplyTo(value) {
3322
+ if (typeof value === "string") {
3323
+ const first = value.split(",")[0]?.trim();
3324
+ return first || void 0;
3325
+ }
3326
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
3327
+ return value[0].trim() || void 0;
3328
+ }
3329
+ return void 0;
3330
+ }
3331
+ function ruleName(fileName) {
3332
+ const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3333
+ return NAME_RE4.test(base) ? base : "imported-rule";
3334
+ }
3335
+ async function importVendorFiles(repoRoot, options = {}) {
3336
+ const include = options.include ? new Set(options.include) : void 0;
3337
+ const wanted = (file) => !include || include.has(file);
3338
+ const prose = [];
3339
+ for (const file of PROSE_PRECEDENCE) {
3340
+ if (!wanted(file)) {
3341
+ continue;
3342
+ }
3343
+ const content = await readIfExists2(path21.join(repoRoot, file));
3344
+ if (content && content.trim()) {
3345
+ prose.push({ file, content });
2968
3346
  }
2969
3347
  }
2970
- return files;
3348
+ const cursorRules = (await listCursorRules(repoRoot)).filter((rule) => wanted(rule.file));
3349
+ let canonicalSource;
3350
+ let canonicalBody = "";
3351
+ let rest = [];
3352
+ if (prose.length > 0) {
3353
+ canonicalSource = prose[0].file;
3354
+ canonicalBody = extractHandAuthored(prose[0].content);
3355
+ rest = prose.slice(1);
3356
+ } else if (cursorRules.length > 0) {
3357
+ canonicalSource = CURSOR_RULES_DIR;
3358
+ canonicalBody = cursorRules.map((rule) => parseFrontmatter(rule.content).body).join("\n\n").trim();
3359
+ }
3360
+ const foldedFrom = [];
3361
+ const skippedDuplicates = [];
3362
+ let body = canonicalBody;
3363
+ for (const file of rest) {
3364
+ const hand = extractHandAuthored(file.content);
3365
+ const novel = novelSections(body, hand);
3366
+ if (novel.length === 0) {
3367
+ skippedDuplicates.push(file.file);
3368
+ continue;
3369
+ }
3370
+ body = `${body}
3371
+
3372
+ <!-- imported from ${file.file} -->
3373
+ ${novel.map((section) => section.text).join("\n\n")}`.trim();
3374
+ foldedFrom.push(file.file);
3375
+ }
3376
+ const importedRules = cursorRules.map((rule) => {
3377
+ const { data, body: ruleBody2 } = parseFrontmatter(rule.content);
3378
+ return {
3379
+ name: ruleName(rule.file),
3380
+ applyTo: globsToApplyTo(data.globs ?? data.applyTo),
3381
+ body: ruleBody2.trim()
3382
+ };
3383
+ });
3384
+ return { canonicalSource, canonicalBody: body, foldedFrom, importedRules, skippedDuplicates };
2971
3385
  }
2972
3386
 
2973
3387
  // src/core/init/index.ts
2974
- import { stringify as stringifyYaml4 } from "yaml";
2975
-
2976
- // src/core/init/builtins.ts
2977
- import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
2978
- import path20 from "path";
2979
- import { fileURLToPath } from "url";
2980
- var DIST_DIR = path20.dirname(fileURLToPath(import.meta.url));
2981
- var PACKAGE_ROOT_FROM_BUNDLE = path20.resolve(DIST_DIR, "..");
2982
- var PACKAGE_ROOT_FROM_DIST = path20.resolve(DIST_DIR, "../../..");
2983
- var PACKAGE_ROOT_FROM_SRC = path20.resolve(DIST_DIR, "../../../..");
2984
- var SKILL_PACK_CANDIDATES = [
2985
- path20.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2986
- path20.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2987
- path20.join(PACKAGE_ROOT_FROM_SRC, "skills")
2988
- ];
2989
- var PROJECT_MEMORY_TEMPLATE = [
2990
- "# Project",
2991
- "",
2992
- "<!-- Stable, rarely-changing facts about this project. Keep it short. -->",
2993
- "",
2994
- "- What it is:",
2995
- "- Key technologies:",
2996
- "- How to run it:"
2997
- ].join("\n");
2998
- async function exists4(target) {
3388
+ var DEFAULT_ADAPTERS = [];
3389
+ var AGENTS_FILE2 = "AGENTS.md";
3390
+ var InitError = class extends Error {
3391
+ constructor(message) {
3392
+ super(message);
3393
+ this.name = "InitError";
3394
+ }
3395
+ };
3396
+ async function pathExists(target) {
2999
3397
  try {
3000
- const info = await stat7(target);
3001
- return info.isDirectory();
3398
+ await stat8(target);
3399
+ return true;
3002
3400
  } catch {
3003
3401
  return false;
3004
3402
  }
3005
3403
  }
3006
- async function bundledSkillsDir() {
3007
- for (const candidate of SKILL_PACK_CANDIDATES) {
3008
- if (await exists4(candidate)) {
3009
- return candidate;
3404
+ async function detectProfile(repoRoot, override) {
3405
+ if (override) {
3406
+ return override;
3407
+ }
3408
+ const files = await walkRepo(repoRoot);
3409
+ const packageJson = await readJson(repoRoot, "package.json");
3410
+ const inferred = inferProfile(files, packageJson);
3411
+ return inferred === "unknown" ? "empty" : inferred;
3412
+ }
3413
+ async function detectName(repoRoot) {
3414
+ const packageJson = await readJson(repoRoot, "package.json");
3415
+ if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
3416
+ return packageJson.name.trim();
3417
+ }
3418
+ return path22.basename(repoRoot);
3419
+ }
3420
+ async function writeManifest(repoRoot, manifest) {
3421
+ const body = {
3422
+ name: manifest.name,
3423
+ version: manifest.version,
3424
+ profile: manifest.profile,
3425
+ adapters: manifest.adapters
3426
+ };
3427
+ if (manifest.tools.allow.length > 0) {
3428
+ body.tools = { allow: manifest.tools.allow };
3429
+ }
3430
+ await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3431
+ await writeFile9(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3432
+ }
3433
+ async function writeProjectMemory(repoRoot) {
3434
+ const dir = projectObjectDir(repoRoot, "memory");
3435
+ await mkdir9(dir, { recursive: true });
3436
+ const filePath = path22.join(dir, "project.md");
3437
+ try {
3438
+ await writeFile9(filePath, `${PROJECT_MEMORY_TEMPLATE}
3439
+ `, { encoding: "utf8", flag: "wx" });
3440
+ return [filePath];
3441
+ } catch (error) {
3442
+ if (error.code === "EEXIST") {
3443
+ return [];
3010
3444
  }
3445
+ throw error;
3011
3446
  }
3012
- throw new Error("Bundled Threadroot skills were not found in this package.");
3013
3447
  }
3014
- async function writeBuiltinSkills(repoRoot) {
3015
- const sourceDir = await bundledSkillsDir();
3016
- const targetDir = projectObjectDir(repoRoot, "skills");
3017
- await mkdir8(targetDir, { recursive: true });
3448
+ async function writeImportedRules(repoRoot, report) {
3449
+ if (report.importedRules.length === 0) {
3450
+ return [];
3451
+ }
3452
+ const dir = projectObjectDir(repoRoot, "rules");
3453
+ await mkdir9(dir, { recursive: true });
3018
3454
  const written = [];
3019
- const entries = await readdir3(sourceDir, { withFileTypes: true });
3020
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
3021
- if (!entry.isDirectory()) {
3022
- continue;
3023
- }
3024
- const sourceSkill = path20.join(sourceDir, entry.name);
3025
- const sourceSkillFile = path20.join(sourceSkill, "SKILL.md");
3026
- if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
3027
- continue;
3455
+ for (const rule of report.importedRules) {
3456
+ const filePath = path22.join(dir, `${rule.name}.md`);
3457
+ const data = { name: rule.name, scope: "project" };
3458
+ if (rule.applyTo) {
3459
+ data.applyTo = rule.applyTo;
3028
3460
  }
3029
- const targetSkill = path20.join(targetDir, entry.name);
3030
- const targetSkillFile = path20.join(targetSkill, "SKILL.md");
3031
3461
  try {
3032
- await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
3033
- written.push(targetSkillFile);
3462
+ await writeFile9(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3463
+ written.push(filePath);
3034
3464
  } catch (error) {
3035
- if (error.code !== "ERR_FS_CP_EEXIST" && error.code !== "EEXIST") {
3465
+ if (error.code !== "EEXIST") {
3036
3466
  throw error;
3037
3467
  }
3038
3468
  }
3039
3469
  }
3040
3470
  return written;
3041
3471
  }
3472
+ async function writeStarterTools(repoRoot, profile, force) {
3473
+ const candidates = await detectToolCandidates(repoRoot, profile);
3474
+ const names = [];
3475
+ for (const candidate of candidates) {
3476
+ try {
3477
+ await createTool(
3478
+ repoRoot,
3479
+ {
3480
+ name: candidate.name,
3481
+ description: candidate.description,
3482
+ run: candidate.run,
3483
+ risk: candidate.risk,
3484
+ confirm: candidate.confirm
3485
+ },
3486
+ { actor: "human", force }
3487
+ );
3488
+ names.push(candidate.name);
3489
+ } catch (error) {
3490
+ if (error instanceof ToolCreateError) {
3491
+ continue;
3492
+ }
3493
+ throw error;
3494
+ }
3495
+ }
3496
+ return names;
3497
+ }
3498
+ async function initHarness(repoRoot, options = {}) {
3499
+ if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
3500
+ throw new InitError(
3501
+ `A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3502
+ );
3503
+ }
3504
+ const profile = await detectProfile(repoRoot, options.profile);
3505
+ const name = await detectName(repoRoot);
3506
+ const adapters = options.adapters ?? DEFAULT_ADAPTERS;
3507
+ const tools2 = await writeStarterTools(repoRoot, profile, options.force ?? false);
3508
+ const skills = await writeBuiltinSkills(repoRoot);
3509
+ const memory = await writeProjectMemory(repoRoot);
3510
+ const manifest = harnessManifestSchema.parse({
3511
+ name,
3512
+ version: 1,
3513
+ profile,
3514
+ adapters,
3515
+ tools: { allow: tools2 }
3516
+ });
3517
+ await writeManifest(repoRoot, manifest);
3518
+ let report;
3519
+ let rules = [];
3520
+ if (options.import !== false) {
3521
+ report = await importVendorFiles(repoRoot, { include: options.importFiles });
3522
+ if (report.canonicalBody.trim()) {
3523
+ await writeFile9(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3524
+ `, "utf8");
3525
+ }
3526
+ rules = await writeImportedRules(repoRoot, report);
3527
+ }
3528
+ const { written } = await runCompile(repoRoot, { home: options.home });
3529
+ const exposed = options.expose ? (await exposeProject(repoRoot, { agents: options.expose })).entries.filter((entry) => entry.status !== "missing" && entry.status !== "skipped").map((entry) => entry.path) : [];
3530
+ return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
3531
+ }
3532
+
3533
+ // src/core/status.ts
3534
+ async function harnessStatus(repoRoot, options = {}) {
3535
+ let harness;
3536
+ try {
3537
+ harness = await resolveHarness(repoRoot, { home: options.home });
3538
+ } catch (error) {
3539
+ if (error instanceof HarnessError) {
3540
+ return { exists: false };
3541
+ }
3542
+ throw error;
3543
+ }
3544
+ const files = await compile(repoRoot, harness);
3545
+ const drift = await detectDrift(repoRoot, files);
3546
+ return {
3547
+ exists: true,
3548
+ manifest: {
3549
+ name: harness.manifest.name,
3550
+ profile: harness.manifest.profile,
3551
+ adapters: harness.manifest.adapters,
3552
+ toolsAllow: harness.manifest.tools.allow
3553
+ },
3554
+ counts: {
3555
+ skills: harness.skills.length,
3556
+ rules: harness.rules.length,
3557
+ tools: harness.tools.length,
3558
+ memory: harness.memory.length
3559
+ },
3560
+ drift
3561
+ };
3562
+ }
3042
3563
 
3043
- // src/core/init/import.ts
3044
- import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
3045
- import path21 from "path";
3046
- var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
3047
- var CURSOR_RULES_DIR = ".cursor/rules";
3048
- var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
3049
- async function readIfExists3(filePath) {
3564
+ // src/core/bootstrap.ts
3565
+ var DEFAULT_TASK = "start this project";
3566
+ async function harnessExists(repoRoot) {
3567
+ return pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3568
+ }
3569
+ async function pathExists2(target) {
3050
3570
  try {
3051
- return await readFile9(filePath, "utf8");
3571
+ await stat9(target);
3572
+ return true;
3052
3573
  } catch (error) {
3053
3574
  if (error.code === "ENOENT") {
3054
- return void 0;
3575
+ return false;
3055
3576
  }
3056
3577
  throw error;
3057
3578
  }
3058
3579
  }
3059
- function normalize(text) {
3060
- return text.toLowerCase().replace(/\s+/g, " ").trim();
3061
- }
3062
- function splitSections(markdown) {
3063
- const sections = [];
3064
- let heading = "";
3065
- let buffer = [];
3066
- const flush = () => {
3067
- const text = buffer.join("\n").trim();
3068
- if (text) {
3069
- sections.push({ heading, text });
3070
- }
3071
- };
3072
- for (const line of markdown.split(/\r?\n/)) {
3073
- if (/^#{1,6}\s/.test(line)) {
3074
- flush();
3075
- heading = normalize(line.replace(/^#+\s*/, ""));
3076
- buffer = [line];
3580
+ function modeFor(options) {
3581
+ return options.yes && !options.dryRun ? "write" : "dry-run";
3582
+ }
3583
+ async function bootstrapProject(repoRoot, options = {}) {
3584
+ const task = options.task?.trim() || DEFAULT_TASK;
3585
+ const mode = modeFor(options);
3586
+ const write2 = mode === "write";
3587
+ const notes = [];
3588
+ const existed = await harnessExists(repoRoot);
3589
+ let setup;
3590
+ let init;
3591
+ let exposed;
3592
+ if (!options.noGlobal) {
3593
+ setup = await setupGlobal({
3594
+ agents: options.agents ?? "all",
3595
+ mode,
3596
+ home: options.home,
3597
+ mcp: options.mcp,
3598
+ mcpEntry: options.mcpEntry
3599
+ });
3600
+ } else {
3601
+ notes.push("Skipped global setup because --no-global was set.");
3602
+ }
3603
+ if (!existed && !options.noInit) {
3604
+ if (write2) {
3605
+ init = await initHarness(repoRoot, {
3606
+ import: options.import,
3607
+ profile: options.profile,
3608
+ home: options.home
3609
+ });
3077
3610
  } else {
3078
- buffer.push(line);
3611
+ notes.push(`Would initialize local-only harness at ${path23.join(".threadroot", "harness.yaml")}.`);
3079
3612
  }
3613
+ } else if (existed) {
3614
+ notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3615
+ } else {
3616
+ notes.push("Skipped project initialization because --no-init was set.");
3080
3617
  }
3081
- flush();
3082
- return sections;
3083
- }
3084
- function novelSections(canonical, other) {
3085
- const haystack = normalize(canonical);
3086
- const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
3087
- return splitSections(other).filter((section) => {
3088
- if (section.heading && seenHeadings.has(section.heading)) {
3089
- return false;
3618
+ const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path23.join(repoRoot, ".threadroot", "harness.yaml"));
3619
+ if (options.expose) {
3620
+ exposed = await exposeProject(repoRoot, {
3621
+ agents: options.expose,
3622
+ mode
3623
+ });
3624
+ }
3625
+ let status;
3626
+ let doctorReport;
3627
+ let context;
3628
+ let mcpCheck;
3629
+ if (options.mcp && write2) {
3630
+ mcpCheck = await checkCodexMcp({ repoRoot, home: options.home });
3631
+ }
3632
+ if (hasHarnessAfterInit) {
3633
+ status = await harnessStatus(repoRoot, { home: options.home });
3634
+ doctorReport = await doctor(repoRoot, { home: options.home });
3635
+ if (status.exists) {
3636
+ context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3090
3637
  }
3091
- return !haystack.includes(normalize(section.text));
3092
- });
3638
+ } else {
3639
+ notes.push("Skipped doctor/status/context because no harness exists yet.");
3640
+ }
3641
+ if (!write2) {
3642
+ notes.push("Run `threadroot bootstrap --yes` to apply this plan.");
3643
+ }
3644
+ return {
3645
+ mode: write2 ? "write" : "plan",
3646
+ task,
3647
+ harnessExisted: existed,
3648
+ setup,
3649
+ init,
3650
+ expose: exposed,
3651
+ status,
3652
+ doctor: doctorReport,
3653
+ context,
3654
+ mcpCheck,
3655
+ notes
3656
+ };
3093
3657
  }
3094
- async function listCursorRules(repoRoot) {
3095
- const dir = path21.join(repoRoot, CURSOR_RULES_DIR);
3096
- let entries;
3097
- try {
3098
- entries = await readdir4(dir);
3099
- } catch (error) {
3100
- if (error.code === "ENOENT") {
3101
- return [];
3102
- }
3103
- throw error;
3658
+ async function startSession(repoRoot, options = {}) {
3659
+ const task = options.task?.trim() || DEFAULT_TASK;
3660
+ const status = await harnessStatus(repoRoot, { home: options.home });
3661
+ const notes = [];
3662
+ if (!status.exists) {
3663
+ return {
3664
+ task,
3665
+ status,
3666
+ notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
3667
+ };
3104
3668
  }
3105
- const files = entries.filter((name) => name.endsWith(".mdc")).sort();
3106
- return Promise.all(
3107
- files.map(async (name) => ({
3108
- file: `${CURSOR_RULES_DIR}/${name}`,
3109
- content: await readFile9(path21.join(dir, name), "utf8")
3110
- }))
3111
- );
3669
+ const doctorReport = await doctor(repoRoot, { home: options.home });
3670
+ const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3671
+ return { task, status, doctor: doctorReport, context, notes };
3112
3672
  }
3113
- function globsToApplyTo(value) {
3114
- if (typeof value === "string") {
3115
- const first = value.split(",")[0]?.trim();
3116
- return first || void 0;
3673
+
3674
+ // src/commands/session-output.ts
3675
+ function printDoctor(report) {
3676
+ if (!report) {
3677
+ return;
3117
3678
  }
3118
- if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
3119
- return value[0].trim() || void 0;
3679
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3680
+ console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
3681
+ for (const finding3 of report.findings.slice(0, 8)) {
3682
+ const label = finding3.severity === "info" ? "hint" : finding3.severity;
3683
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3684
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3685
+ }
3686
+ if (report.findings.length > 8) {
3687
+ console.log(`- ... ${report.findings.length - 8} more finding(s)`);
3120
3688
  }
3121
- return void 0;
3122
3689
  }
3123
- function ruleName(fileName) {
3124
- const base = path21.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3125
- return NAME_RE4.test(base) ? base : "imported-rule";
3690
+ function printStatus(status) {
3691
+ if (!status) {
3692
+ return;
3693
+ }
3694
+ if (!status.exists) {
3695
+ console.log("harness: missing");
3696
+ return;
3697
+ }
3698
+ console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3699
+ console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
3700
+ console.log(
3701
+ `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3702
+ );
3126
3703
  }
3127
- async function importVendorFiles(repoRoot, options = {}) {
3128
- const include = options.include ? new Set(options.include) : void 0;
3129
- const wanted = (file) => !include || include.has(file);
3130
- const prose = [];
3131
- for (const file of PROSE_PRECEDENCE) {
3132
- if (!wanted(file)) {
3133
- continue;
3704
+ function printContext(context) {
3705
+ if (!context) {
3706
+ return;
3707
+ }
3708
+ console.log(`task: ${context.task}`);
3709
+ if (context.skills.length > 0) {
3710
+ const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
3711
+ console.log(skillLabel);
3712
+ for (const skill of context.skills.slice(0, 8)) {
3713
+ console.log(`- ${skill.name} - ${skill.when}`);
3134
3714
  }
3135
- const content = await readIfExists3(path21.join(repoRoot, file));
3136
- if (content && content.trim()) {
3137
- prose.push({ file, content });
3715
+ } else {
3716
+ console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
3717
+ }
3718
+ if (context.tools.length > 0) {
3719
+ console.log("available tools:");
3720
+ for (const tool of context.tools.slice(0, 8)) {
3721
+ console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
3138
3722
  }
3139
3723
  }
3140
- const cursorRules = (await listCursorRules(repoRoot)).filter((rule) => wanted(rule.file));
3141
- let canonicalSource;
3142
- let canonicalBody = "";
3143
- let rest = [];
3144
- if (prose.length > 0) {
3145
- canonicalSource = prose[0].file;
3146
- canonicalBody = extractHandAuthored(prose[0].content);
3147
- rest = prose.slice(1);
3148
- } else if (cursorRules.length > 0) {
3149
- canonicalSource = CURSOR_RULES_DIR;
3150
- canonicalBody = cursorRules.map((rule) => parseFrontmatter(rule.content).body).join("\n\n").trim();
3724
+ if (context.memory.length > 0) {
3725
+ console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
3151
3726
  }
3152
- const foldedFrom = [];
3153
- const skippedDuplicates = [];
3154
- let body = canonicalBody;
3155
- for (const file of rest) {
3156
- const hand = extractHandAuthored(file.content);
3157
- const novel = novelSections(body, hand);
3158
- if (novel.length === 0) {
3159
- skippedDuplicates.push(file.file);
3160
- continue;
3727
+ }
3728
+ function printCommandMap() {
3729
+ console.log("agent command map:");
3730
+ console.log('- `threadroot start "<task>"` - begin a focused agent session');
3731
+ console.log('- `threadroot context "<task>"` - get relevant skills, tools, rules, and memory');
3732
+ console.log("- `threadroot doctor` - check harness health and trust issues");
3733
+ console.log("- `threadroot skills list|inspect|validate` - inspect skill capabilities");
3734
+ console.log("- `threadroot tools list|check` and `threadroot run <tool>` - use explicit local tools");
3735
+ console.log('- `threadroot remember "<note>"` - save durable handoff/project memory');
3736
+ }
3737
+ function printMcpCheck(report) {
3738
+ if (!report) {
3739
+ return;
3740
+ }
3741
+ console.log(`mcp check: ${report.status}`);
3742
+ if (report.entry) {
3743
+ console.log(`mcp server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
3744
+ }
3745
+ for (const message of report.messages) {
3746
+ console.log(`- ${message}`);
3747
+ }
3748
+ }
3749
+ function printBootstrapReport(report) {
3750
+ console.log(`Threadroot bootstrap: ${report.mode === "write" ? "complete" : "plan"}`);
3751
+ if (report.setup) {
3752
+ console.log("global setup:");
3753
+ for (const entry of report.setup.entries) {
3754
+ const suffix = entry.message ? ` - ${entry.message}` : "";
3755
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3161
3756
  }
3162
- body = `${body}
3163
-
3164
- <!-- imported from ${file.file} -->
3165
- ${novel.map((section) => section.text).join("\n\n")}`.trim();
3166
- foldedFrom.push(file.file);
3167
3757
  }
3168
- const importedRules = cursorRules.map((rule) => {
3169
- const { data, body: ruleBody2 } = parseFrontmatter(rule.content);
3170
- return {
3171
- name: ruleName(rule.file),
3172
- applyTo: globsToApplyTo(data.globs ?? data.applyTo),
3173
- body: ruleBody2.trim()
3174
- };
3758
+ if (report.init) {
3759
+ console.log("project init: created local-only .threadroot/");
3760
+ } else if (report.harnessExisted) {
3761
+ console.log("project init: existing harness preserved");
3762
+ }
3763
+ if (report.expose) {
3764
+ console.log("project exposure:");
3765
+ for (const entry of report.expose.entries) {
3766
+ const suffix = entry.message ? ` - ${entry.message}` : "";
3767
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3768
+ }
3769
+ }
3770
+ printStatus(report.status);
3771
+ printMcpCheck(report.mcpCheck);
3772
+ printDoctor(report.doctor);
3773
+ printContext(report.context);
3774
+ printCommandMap();
3775
+ for (const note of report.notes) {
3776
+ console.log(`note: ${note}`);
3777
+ }
3778
+ if (report.mode === "write") {
3779
+ console.log('Success: Threadroot is ready. Run `threadroot start "<task>"` for future sessions.');
3780
+ }
3781
+ }
3782
+ function printStartReport(report) {
3783
+ console.log("Threadroot start:");
3784
+ printStatus(report.status);
3785
+ printDoctor(report.doctor);
3786
+ printContext(report.context);
3787
+ printCommandMap();
3788
+ for (const note of report.notes) {
3789
+ console.log(`note: ${note}`);
3790
+ }
3791
+ }
3792
+
3793
+ // src/commands/bootstrap.ts
3794
+ async function runBootstrap(repoRoot, options) {
3795
+ const report = await bootstrapProject(repoRoot, {
3796
+ yes: options.yes,
3797
+ dryRun: options.dryRun,
3798
+ agents: options.agent,
3799
+ task: options.task,
3800
+ mcp: options.mcp,
3801
+ expose: options.expose,
3802
+ noGlobal: options.global === false,
3803
+ noInit: options.init === false,
3804
+ import: options.import,
3805
+ profile: options.profile ? profileIdSchema.parse(options.profile) : void 0,
3806
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
3175
3807
  });
3176
- return { canonicalSource, canonicalBody: body, foldedFrom, importedRules, skippedDuplicates };
3808
+ printBootstrapReport(report);
3809
+ if (report.mode === "write" && report.doctor && !report.doctor.ok) {
3810
+ process.exitCode = 1;
3811
+ }
3177
3812
  }
3178
3813
 
3179
- // src/core/init/index.ts
3180
- var DEFAULT_ADAPTERS = [];
3181
- var AGENTS_FILE2 = "AGENTS.md";
3182
- var InitError = class extends Error {
3183
- constructor(message) {
3184
- super(message);
3185
- this.name = "InitError";
3814
+ // src/commands/compile.ts
3815
+ async function runCompileCommand(repoRoot, options) {
3816
+ const adapter = options.adapter ? adapterIdSchema.parse(options.adapter) : void 0;
3817
+ try {
3818
+ const { written, drift } = await runCompile(repoRoot, { adapter });
3819
+ const changed = drift.filter((entry) => entry.status !== "unchanged").length;
3820
+ console.log(`Compiled ${written.length} vendor file(s)${changed > 0 ? ` (${changed} changed)` : ""}.`);
3821
+ for (const file of written) {
3822
+ console.log(` ${file}`);
3823
+ }
3824
+ } catch (error) {
3825
+ if (error instanceof HarnessError) {
3826
+ console.error(error.message);
3827
+ process.exitCode = 1;
3828
+ return;
3829
+ }
3830
+ throw error;
3186
3831
  }
3187
- };
3188
- async function pathExists(target) {
3832
+ }
3833
+
3834
+ // src/commands/context.ts
3835
+ async function runContext(repoRoot, task) {
3836
+ let context;
3189
3837
  try {
3190
- await stat8(target);
3191
- return true;
3192
- } catch {
3193
- return false;
3838
+ context = await assembleContext(repoRoot, task);
3839
+ } catch (error) {
3840
+ if (error instanceof HarnessError) {
3841
+ console.log("No harness found. Run `tr init` first.");
3842
+ return;
3843
+ }
3844
+ throw error;
3845
+ }
3846
+ console.log(`task: ${context.task}`);
3847
+ if (context.skills.length > 0) {
3848
+ console.log("\nskills:");
3849
+ for (const skill of context.skills) {
3850
+ console.log(`- ${skill.name} - ${skill.when}`);
3851
+ }
3852
+ }
3853
+ if (context.rules.length > 0) {
3854
+ console.log("\nrules:");
3855
+ for (const rule of context.rules) {
3856
+ console.log(`- ${rule.name}${rule.applyTo ? ` (${rule.applyTo})` : ""}`);
3857
+ }
3858
+ }
3859
+ if (context.tools.length > 0) {
3860
+ console.log("\ntools:");
3861
+ for (const tool of context.tools) {
3862
+ console.log(`- ${tool.name} - ${tool.description}`);
3863
+ }
3864
+ }
3865
+ if (context.memory.length > 0) {
3866
+ console.log("\nmemory:");
3867
+ for (const entry of context.memory) {
3868
+ console.log(`- ${entry.type}`);
3869
+ }
3870
+ }
3871
+ if (context.skills.length === 0 && context.rules.length === 0 && context.tools.length === 0 && context.memory.length === 0) {
3872
+ console.log("\nNo matching harness context for this task yet.");
3194
3873
  }
3195
3874
  }
3196
- async function detectProfile(repoRoot, override) {
3197
- if (override) {
3198
- return override;
3875
+
3876
+ // src/commands/diff.ts
3877
+ import fs3 from "fs/promises";
3878
+ import path24 from "path";
3879
+ async function readIfExists3(filePath) {
3880
+ try {
3881
+ return await fs3.readFile(filePath, "utf8");
3882
+ } catch (error) {
3883
+ if (error.code === "ENOENT") {
3884
+ return void 0;
3885
+ }
3886
+ throw error;
3199
3887
  }
3200
- const files = await walkRepo(repoRoot);
3201
- const packageJson = await readJson(repoRoot, "package.json");
3202
- const inferred = inferProfile(files, packageJson);
3203
- return inferred === "unknown" ? "empty" : inferred;
3204
3888
  }
3205
- async function detectName(repoRoot) {
3206
- const packageJson = await readJson(repoRoot, "package.json");
3207
- if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
3208
- return packageJson.name.trim();
3889
+ function lineDiff(before, after) {
3890
+ const a = before.length === 0 ? [] : before.split("\n");
3891
+ const b = after.length === 0 ? [] : after.split("\n");
3892
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
3893
+ for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
3894
+ for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
3895
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
3896
+ }
3209
3897
  }
3210
- return path22.basename(repoRoot);
3211
- }
3212
- async function writeManifest(repoRoot, manifest) {
3213
- const body = {
3214
- name: manifest.name,
3215
- version: manifest.version,
3216
- profile: manifest.profile,
3217
- adapters: manifest.adapters
3218
- };
3219
- if (manifest.tools.allow.length > 0) {
3220
- body.tools = { allow: manifest.tools.allow };
3898
+ const lines = [];
3899
+ let i = 0;
3900
+ let j = 0;
3901
+ while (i < a.length && j < b.length) {
3902
+ if (a[i] === b[j]) {
3903
+ i += 1;
3904
+ j += 1;
3905
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
3906
+ lines.push(`- ${a[i]}`);
3907
+ i += 1;
3908
+ } else {
3909
+ lines.push(`+ ${b[j]}`);
3910
+ j += 1;
3911
+ }
3221
3912
  }
3222
- await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3223
- await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3913
+ while (i < a.length) {
3914
+ lines.push(`- ${a[i]}`);
3915
+ i += 1;
3916
+ }
3917
+ while (j < b.length) {
3918
+ lines.push(`+ ${b[j]}`);
3919
+ j += 1;
3920
+ }
3921
+ return lines;
3224
3922
  }
3225
- async function writeProjectMemory(repoRoot) {
3226
- const dir = projectObjectDir(repoRoot, "memory");
3227
- await mkdir9(dir, { recursive: true });
3228
- const filePath = path22.join(dir, "project.md");
3923
+ async function runDiff(repoRoot) {
3924
+ let harness;
3229
3925
  try {
3230
- await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
3231
- `, { encoding: "utf8", flag: "wx" });
3232
- return [filePath];
3926
+ harness = await resolveHarness(repoRoot);
3233
3927
  } catch (error) {
3234
- if (error.code === "EEXIST") {
3235
- return [];
3928
+ if (error instanceof HarnessError) {
3929
+ console.log("No harness found. Run `tr init` first.");
3930
+ return;
3236
3931
  }
3237
3932
  throw error;
3238
3933
  }
3239
- }
3240
- async function writeImportedRules(repoRoot, report) {
3241
- if (report.importedRules.length === 0) {
3242
- return [];
3243
- }
3244
- const dir = projectObjectDir(repoRoot, "rules");
3245
- await mkdir9(dir, { recursive: true });
3246
- const written = [];
3247
- for (const rule of report.importedRules) {
3248
- const filePath = path22.join(dir, `${rule.name}.md`);
3249
- const data = { name: rule.name, scope: "project" };
3250
- if (rule.applyTo) {
3251
- data.applyTo = rule.applyTo;
3934
+ const files = await compile(repoRoot, harness);
3935
+ let changed = 0;
3936
+ for (const file of files) {
3937
+ const existing = await readIfExists3(path24.join(repoRoot, file.path));
3938
+ if (existing === void 0) {
3939
+ changed += 1;
3940
+ console.log(`+ ${file.path} (new)`);
3941
+ continue;
3252
3942
  }
3253
- try {
3254
- await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3255
- written.push(filePath);
3256
- } catch (error) {
3257
- if (error.code !== "EEXIST") {
3258
- throw error;
3259
- }
3943
+ if (existing === file.content) {
3944
+ continue;
3945
+ }
3946
+ changed += 1;
3947
+ console.log(`~ ${file.path}`);
3948
+ for (const line of lineDiff(existing, file.content)) {
3949
+ console.log(` ${line}`);
3260
3950
  }
3261
3951
  }
3262
- return written;
3952
+ if (changed === 0) {
3953
+ console.log("No drift: optional compiled outputs match the canonical harness.");
3954
+ }
3263
3955
  }
3264
- async function writeStarterTools(repoRoot, profile, force) {
3265
- const candidates = await detectToolCandidates(repoRoot, profile);
3266
- const names = [];
3267
- for (const candidate of candidates) {
3268
- try {
3269
- await createTool(
3270
- repoRoot,
3271
- {
3272
- name: candidate.name,
3273
- description: candidate.description,
3274
- run: candidate.run,
3275
- risk: candidate.risk,
3276
- confirm: candidate.confirm
3277
- },
3278
- { actor: "human", force }
3279
- );
3280
- names.push(candidate.name);
3281
- } catch (error) {
3282
- if (error instanceof ToolCreateError) {
3283
- continue;
3284
- }
3285
- throw error;
3956
+
3957
+ // src/commands/doctor.ts
3958
+ async function runDoctor(repoRoot) {
3959
+ const report = await doctor(repoRoot);
3960
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3961
+ const hints = report.findings.filter((finding3) => finding3.severity === "info");
3962
+ if (actionable.length === 0) {
3963
+ console.log("Threadroot doctor: clean");
3964
+ for (const finding3 of hints) {
3965
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3966
+ console.log(`- hint ${finding3.code}: ${finding3.message}${suffix}`);
3286
3967
  }
3968
+ return;
3287
3969
  }
3288
- return names;
3289
- }
3290
- async function initHarness(repoRoot, options = {}) {
3291
- if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
3292
- throw new InitError(
3293
- `A harness already exists at ${path22.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3294
- );
3970
+ console.log(
3971
+ `Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`
3972
+ );
3973
+ for (const finding3 of report.findings) {
3974
+ const label = finding3.severity === "error" ? "error" : finding3.severity === "warning" ? "warning" : "hint";
3975
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3976
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3295
3977
  }
3296
- const profile = await detectProfile(repoRoot, options.profile);
3297
- const name = await detectName(repoRoot);
3298
- const adapters = options.adapters ?? DEFAULT_ADAPTERS;
3299
- const tools2 = await writeStarterTools(repoRoot, profile, options.force ?? false);
3300
- const skills = await writeBuiltinSkills(repoRoot);
3301
- const memory = await writeProjectMemory(repoRoot);
3302
- const manifest = harnessManifestSchema.parse({
3303
- name,
3304
- version: 1,
3305
- profile,
3306
- adapters,
3307
- tools: { allow: tools2 }
3978
+ if (!report.ok) {
3979
+ process.exitCode = 1;
3980
+ }
3981
+ }
3982
+
3983
+ // src/commands/expose.ts
3984
+ function modeFromOptions(options) {
3985
+ if (options.undo) return "undo";
3986
+ if (options.check) return "check";
3987
+ if (options.dryRun) return "dry-run";
3988
+ return "write";
3989
+ }
3990
+ async function runExpose(repoRoot, agent, options) {
3991
+ const mode = modeFromOptions(options);
3992
+ const result = await exposeProject(repoRoot, {
3993
+ agents: agent,
3994
+ mode,
3995
+ force: options.force
3308
3996
  });
3309
- await writeManifest(repoRoot, manifest);
3310
- let report;
3311
- let rules = [];
3312
- if (options.import !== false) {
3313
- report = await importVendorFiles(repoRoot, { include: options.importFiles });
3314
- if (report.canonicalBody.trim()) {
3315
- await writeFile8(path22.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3316
- `, "utf8");
3317
- }
3318
- rules = await writeImportedRules(repoRoot, report);
3997
+ const verb = mode === "dry-run" ? "Project exposure plan" : mode === "check" ? "Project exposure check" : mode === "undo" ? "Removed project exposure" : "Exposed Threadroot project skills";
3998
+ console.log(`${verb}:`);
3999
+ for (const entry of result.entries) {
4000
+ const suffix = entry.message ? ` - ${entry.message}` : "";
4001
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3319
4002
  }
3320
- const { written } = await runCompile(repoRoot, { home: options.home });
3321
- const exposed = options.expose ? (await exposeProject(repoRoot, { agents: options.expose })).entries.filter((entry) => entry.status !== "missing" && entry.status !== "skipped").map((entry) => entry.path) : [];
3322
- return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
3323
4003
  }
3324
4004
 
3325
4005
  // src/commands/init.ts
@@ -3369,13 +4049,13 @@ async function runInit(repoRoot, options) {
3369
4049
  }
3370
4050
 
3371
4051
  // src/commands/install.ts
3372
- import path25 from "path";
4052
+ import path27 from "path";
3373
4053
 
3374
4054
  // src/core/install/fetch.ts
3375
4055
  import { execFile } from "child_process";
3376
- import { mkdtemp, rm as rm3 } from "fs/promises";
4056
+ import { mkdtemp as mkdtemp2, rm as rm4 } from "fs/promises";
3377
4057
  import os2 from "os";
3378
- import path23 from "path";
4058
+ import path25 from "path";
3379
4059
  import { promisify } from "util";
3380
4060
  var run = promisify(execFile);
3381
4061
  function cloneUrl(ref) {
@@ -3396,8 +4076,8 @@ async function git(cwd, args) {
3396
4076
  }
3397
4077
  async function fetchGitSource(ref) {
3398
4078
  const url = cloneUrl(ref);
3399
- const dir = await mkdtemp(path23.join(os2.tmpdir(), "threadroot-fetch-"));
3400
- const cleanup = () => rm3(dir, { recursive: true, force: true });
4079
+ const dir = await mkdtemp2(path25.join(os2.tmpdir(), "threadroot-fetch-"));
4080
+ const cleanup = () => rm4(dir, { recursive: true, force: true });
3401
4081
  try {
3402
4082
  if (ref.ref) {
3403
4083
  try {
@@ -3418,9 +4098,9 @@ async function fetchGitSource(ref) {
3418
4098
  }
3419
4099
 
3420
4100
  // src/core/install/install.ts
3421
- import { cp as cp2, mkdir as mkdir10, readFile as readFile10, readdir as readdir5, stat as stat9, writeFile as writeFile9 } from "fs/promises";
4101
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile11, readdir as readdir5, stat as stat10, writeFile as writeFile10 } from "fs/promises";
3422
4102
  import { createHash as createHash2 } from "crypto";
3423
- import path24 from "path";
4103
+ import path26 from "path";
3424
4104
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
3425
4105
  var KIND_DIR = {
3426
4106
  skill: "skills",
@@ -3432,8 +4112,8 @@ function objectExt(kind) {
3432
4112
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
3433
4113
  }
3434
4114
  function safeRepoPath(objectPath) {
3435
- const normalized = path24.normalize(objectPath);
3436
- if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
4115
+ const normalized = path26.normalize(objectPath);
4116
+ if (path26.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path26.sep}`)) {
3437
4117
  throw new Error(`Unsafe object path: ${objectPath}`);
3438
4118
  }
3439
4119
  return normalized;
@@ -3447,7 +4127,7 @@ function inferKind(objectPath, override) {
3447
4127
  if (segments.includes("tools")) return "tool";
3448
4128
  if (segments.includes("connections")) return "connection";
3449
4129
  if (segments.includes("rules")) return "rule";
3450
- const ext = path24.extname(objectPath).toLowerCase();
4130
+ const ext = path26.extname(objectPath).toLowerCase();
3451
4131
  if (ext === ".yaml" || ext === ".yml") return "tool";
3452
4132
  if (ext === ".md") return "skill";
3453
4133
  throw new Error(
@@ -3455,7 +4135,7 @@ function inferKind(objectPath, override) {
3455
4135
  );
3456
4136
  }
3457
4137
  function deriveName(objectPath) {
3458
- const base = path24.basename(objectPath, path24.extname(objectPath));
4138
+ const base = path26.basename(objectPath, path26.extname(objectPath));
3459
4139
  if (!NAME_RE5.test(base)) {
3460
4140
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
3461
4141
  }
@@ -3467,8 +4147,8 @@ async function hashDirectory(root) {
3467
4147
  async function walk(dir) {
3468
4148
  const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3469
4149
  for (const entry of entries) {
3470
- const full = path24.join(dir, entry.name);
3471
- const rel = path24.relative(root, full).split(path24.sep).join("/");
4150
+ const full = path26.join(dir, entry.name);
4151
+ const rel = path26.relative(root, full).split(path26.sep).join("/");
3472
4152
  if (entry.isSymbolicLink()) {
3473
4153
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
3474
4154
  }
@@ -3479,7 +4159,7 @@ async function hashDirectory(root) {
3479
4159
  if (entry.isFile()) {
3480
4160
  hash.update(`file:${rel}
3481
4161
  `);
3482
- hash.update(await readFile10(full));
4162
+ hash.update(await readFile11(full));
3483
4163
  hash.update("\n");
3484
4164
  }
3485
4165
  }
@@ -3488,8 +4168,8 @@ async function hashDirectory(root) {
3488
4168
  return hash.digest("hex");
3489
4169
  }
3490
4170
  async function validateSkillDirectory2(sourcePath, expectedName) {
3491
- const skillPath = path24.join(sourcePath, "SKILL.md");
3492
- const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
4171
+ const skillPath = path26.join(sourcePath, "SKILL.md");
4172
+ const parsed = parseFrontmatter(await readFile11(skillPath, "utf8"));
3493
4173
  const result = skillFrontmatterSchema.safeParse(parsed.data);
3494
4174
  if (!result.success) {
3495
4175
  const detail = result.error.issues.map((issue) => issue.message).join("; ");
@@ -3518,7 +4198,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
3518
4198
  objectPath = safeRepoPath(within);
3519
4199
  refLabel = ref.ref;
3520
4200
  const fetched = await fetchGitSource(ref);
3521
- sourcePath = path24.join(fetched.dir, objectPath);
4201
+ sourcePath = path26.join(fetched.dir, objectPath);
3522
4202
  resolved = fetched.sha;
3523
4203
  cleanup = fetched.cleanup;
3524
4204
  } else {
@@ -3533,20 +4213,20 @@ async function installObject(repoRoot, rawSource, options = {}) {
3533
4213
  const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
3534
4214
  let destPath;
3535
4215
  let integrity;
3536
- const info = await stat9(sourcePath);
4216
+ const info = await stat10(sourcePath);
3537
4217
  if (info.isDirectory()) {
3538
4218
  if (kind !== "skill") {
3539
4219
  throw new Error("Only skill objects may be installed from a directory.");
3540
4220
  }
3541
4221
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3542
- destPath = path24.join(destDir, name);
4222
+ destPath = path26.join(destDir, name);
3543
4223
  await mkdir10(destDir, { recursive: true });
3544
4224
  await cp2(sourcePath, destPath, { recursive: true, force: true });
3545
4225
  } else {
3546
- const content = await readFile10(sourcePath, "utf8");
3547
- destPath = path24.join(destDir, `${name}${objectExt(kind)}`);
4226
+ const content = await readFile11(sourcePath, "utf8");
4227
+ destPath = path26.join(destDir, `${name}${objectExt(kind)}`);
3548
4228
  await mkdir10(destDir, { recursive: true });
3549
- await writeFile9(destPath, content, "utf8");
4229
+ await writeFile10(destPath, content, "utf8");
3550
4230
  integrity = `sha256:${hashContent(content)}`;
3551
4231
  }
3552
4232
  const entry = {
@@ -3598,7 +4278,7 @@ async function runInstall(repoRoot, source, options) {
3598
4278
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3599
4279
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3600
4280
  if (scope === "project") {
3601
- console.log(` inspect: threadroot skills inspect ${path25.relative(repoRoot, installed.path)}`);
4281
+ console.log(` inspect: threadroot skills inspect ${path27.relative(repoRoot, installed.path)}`);
3602
4282
  }
3603
4283
  }
3604
4284
  } catch (error) {
@@ -3611,39 +4291,6 @@ async function runInstall(repoRoot, source, options) {
3611
4291
  import readline from "readline";
3612
4292
  import { stdin as input, stdout as output } from "process";
3613
4293
  import { z as z4 } from "zod";
3614
-
3615
- // src/core/status.ts
3616
- async function harnessStatus(repoRoot, options = {}) {
3617
- let harness;
3618
- try {
3619
- harness = await resolveHarness(repoRoot, { home: options.home });
3620
- } catch (error) {
3621
- if (error instanceof HarnessError) {
3622
- return { exists: false };
3623
- }
3624
- throw error;
3625
- }
3626
- const files = await compile(repoRoot, harness);
3627
- const drift = await detectDrift(repoRoot, files);
3628
- return {
3629
- exists: true,
3630
- manifest: {
3631
- name: harness.manifest.name,
3632
- profile: harness.manifest.profile,
3633
- adapters: harness.manifest.adapters,
3634
- toolsAllow: harness.manifest.tools.allow
3635
- },
3636
- counts: {
3637
- skills: harness.skills.length,
3638
- rules: harness.rules.length,
3639
- tools: harness.tools.length,
3640
- memory: harness.memory.length
3641
- },
3642
- drift
3643
- };
3644
- }
3645
-
3646
- // src/mcp/server.ts
3647
4294
  function defineTool(spec) {
3648
4295
  return spec;
3649
4296
  }
@@ -3922,8 +4569,9 @@ async function handleMessage(repoRoot, request) {
3922
4569
  if (request.method === "initialize") {
3923
4570
  return resultResponse(request, {
3924
4571
  protocolVersion: "2024-11-05",
3925
- serverInfo: { name: "threadroot", version: "0.1.1" },
3926
- capabilities: { tools: {} }
4572
+ serverInfo: { name: "threadroot", version: "0.1.3" },
4573
+ capabilities: { tools: {} },
4574
+ instructions: "Threadroot exposes the repository's AI agent harness. Call `context` before broad coding work, `doctor` for health and trust checks, inspect skills/tools before risky actions, and use `memory_append` for durable handoffs."
3927
4575
  });
3928
4576
  }
3929
4577
  if (request.method === "notifications/initialized") {
@@ -4064,29 +4712,28 @@ Repository:
4064
4712
  ${repoRoot}
4065
4713
 
4066
4714
  Goal:
4067
- Initialize Threadroot so this repo has a portable AI-agent harness in one canonical \`.threadroot/\` directory.
4715
+ Make this repository ready for agent-assisted development with minimal project clutter.
4068
4716
 
4069
4717
  Rules:
4070
4718
  - Prefer deterministic CLI commands.
4071
- - Do not overwrite user-owned files without checking Threadroot status/diff.
4719
+ - Do not invent Threadroot commands.
4072
4720
  - Keep context small. Use Threadroot context output before reading broad project files.
4721
+ - Do not create provider-specific project files unless the user asks.
4073
4722
 
4074
4723
  Steps:
4075
4724
  1. Check whether Threadroot is available with \`threadroot --version\`.
4076
4725
  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\`.
4077
- 3. Run \`threadroot status\` to check whether a harness already exists.
4078
- 4. If no harness exists, run \`threadroot init\`. Use \`--no-import\` only when the user explicitly wants a blank-slate harness.
4079
- 5. Run \`threadroot status\` again.
4080
- 6. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
4081
- 7. Run \`threadroot context "current task"\` with the user's actual task to find relevant skills, rules, tools, and memory.
4082
- 8. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
4726
+ 3. Run \`threadroot bootstrap --yes --agent all --task "current task"\`. If this is a local checkout, run \`${localCommand} bootstrap --yes --agent all --task "current task"\`.
4727
+ 4. Run \`threadroot start "current task"\` with the user's actual task.
4728
+ 5. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
4729
+ 6. If project-local MCP config is useful, ask before running \`threadroot mcp setup --write\`, then tell the user to reload their agent surface.
4083
4730
 
4084
4731
  Final response:
4085
4732
  Say exactly:
4086
- "Success: Threadroot is initialized. Run \`threadroot status\` or \`threadroot context "<task>"\` to use it."
4733
+ "Success: Threadroot is ready. Run \`threadroot start "<task>"\` for future sessions."
4087
4734
 
4088
4735
  If using a local checkout instead of an installed package, say:
4089
- "Success: Threadroot is initialized. Run \`${localCommand} status\` or \`${localCommand} context "<task>"\` to use it."`;
4736
+ "Success: Threadroot is ready. Run \`${localCommand} start "<task>"\` for future sessions."`;
4090
4737
  }
4091
4738
  function parseAgent(value) {
4092
4739
  if (value === "codex" || value === "copilot" || value === "cursor" || value === "claude" || value === "generic") {
@@ -4146,11 +4793,11 @@ function agentNotes(agent) {
4146
4793
  }
4147
4794
 
4148
4795
  // src/core/mcp-config.ts
4149
- import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
4150
- import path26 from "path";
4796
+ import { mkdir as mkdir11, readFile as readFile12, writeFile as writeFile11 } from "fs/promises";
4797
+ import path28 from "path";
4151
4798
  var TARGETS = [
4152
- { agent: "copilot", file: path26.join(".vscode", "mcp.json"), key: "servers" },
4153
- { agent: "cursor", file: path26.join(".cursor", "mcp.json"), key: "mcpServers" },
4799
+ { agent: "copilot", file: path28.join(".vscode", "mcp.json"), key: "servers" },
4800
+ { agent: "cursor", file: path28.join(".cursor", "mcp.json"), key: "mcpServers" },
4154
4801
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4155
4802
  ];
4156
4803
  function mcpServerEntry(command, scriptPath) {
@@ -4159,7 +4806,7 @@ function mcpServerEntry(command, scriptPath) {
4159
4806
  async function mergeConfig(filePath, key, entry) {
4160
4807
  let config = {};
4161
4808
  try {
4162
- const raw = await readFile11(filePath, "utf8");
4809
+ const raw = await readFile12(filePath, "utf8");
4163
4810
  const parsed = JSON.parse(raw);
4164
4811
  if (parsed && typeof parsed === "object") {
4165
4812
  config = parsed;
@@ -4172,8 +4819,8 @@ async function mergeConfig(filePath, key, entry) {
4172
4819
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4173
4820
  servers.threadroot = { ...entry };
4174
4821
  config[key] = servers;
4175
- await mkdir11(path26.dirname(filePath), { recursive: true });
4176
- await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
4822
+ await mkdir11(path28.dirname(filePath), { recursive: true });
4823
+ await writeFile11(filePath, `${JSON.stringify(config, null, 2)}
4177
4824
  `, "utf8");
4178
4825
  }
4179
4826
  async function writeProjectMcpConfigs(input2) {
@@ -4181,7 +4828,7 @@ async function writeProjectMcpConfigs(input2) {
4181
4828
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4182
4829
  const written = [];
4183
4830
  for (const target of targets) {
4184
- const filePath = path26.join(input2.repoRoot, target.file);
4831
+ const filePath = path28.join(input2.repoRoot, target.file);
4185
4832
  await mergeConfig(filePath, target.key, input2.entry);
4186
4833
  written.push(target.file);
4187
4834
  }
@@ -4215,6 +4862,24 @@ async function runMcpSetup(repoRoot, options) {
4215
4862
  }
4216
4863
  console.log(mcpSetupGuide({ repoRoot, agent: options.agent }));
4217
4864
  }
4865
+ async function runMcpCheck(repoRoot, options) {
4866
+ const timeoutMs = options.timeout ? Number.parseInt(options.timeout, 10) : void 0;
4867
+ const report = await checkCodexMcp({ repoRoot, timeoutMs });
4868
+ console.log(`Threadroot MCP check: ${report.status}`);
4869
+ console.log(`config: ${report.configPath}`);
4870
+ if (report.entry) {
4871
+ console.log(`server: ${report.entry.command} ${report.entry.args.join(" ")}`.trim());
4872
+ }
4873
+ for (const message of report.messages) {
4874
+ console.log(`- ${message}`);
4875
+ }
4876
+ if (report.tools.length > 0) {
4877
+ console.log(`tools: ${report.tools.join(", ")}`);
4878
+ }
4879
+ if (report.status === "error") {
4880
+ process.exitCode = 1;
4881
+ }
4882
+ }
4218
4883
 
4219
4884
  // src/commands/memory.ts
4220
4885
  async function runMemoryRead(repoRoot, type) {
@@ -4301,7 +4966,8 @@ async function runSetup(_repoRoot, options) {
4301
4966
  agents: options.agent,
4302
4967
  mode,
4303
4968
  force: options.force,
4304
- mcp: options.mcp
4969
+ mcp: options.mcp,
4970
+ mcpEntry: options.mcp ? mcpEntryForCurrentProcess() : void 0
4305
4971
  });
4306
4972
  const title = mode === "dry-run" ? "Global setup plan" : mode === "check" ? "Global setup check" : mode === "undo" ? "Global setup undo" : "Global setup complete";
4307
4973
  console.log(`${title}:`);
@@ -4309,11 +4975,33 @@ async function runSetup(_repoRoot, options) {
4309
4975
  const suffix = entry.message ? ` - ${entry.message}` : "";
4310
4976
  console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
4311
4977
  }
4978
+ if (options.mcp && options.global && mode === "write") {
4979
+ const check = await checkCodexMcp({ repoRoot: _repoRoot });
4980
+ console.log(`MCP verification: ${check.status}`);
4981
+ if (check.entry) {
4982
+ console.log(`MCP server: ${check.entry.command} ${check.entry.args.join(" ")}`.trim());
4983
+ }
4984
+ for (const message of check.messages) {
4985
+ console.log(`- ${message}`);
4986
+ }
4987
+ if (check.status === "error") {
4988
+ process.exitCode = 1;
4989
+ }
4990
+ }
4312
4991
  if (mode === "write") {
4313
4992
  console.log("Reload or restart open agent sessions so new global skills/config are discovered.");
4314
4993
  }
4315
4994
  }
4316
4995
 
4996
+ // src/commands/start.ts
4997
+ async function runStart(repoRoot, task, options) {
4998
+ const report = await startSession(repoRoot, { task: task ?? options.task });
4999
+ printStartReport(report);
5000
+ if (!report.status.exists || report.doctor && !report.doctor.ok) {
5001
+ process.exitCode = 1;
5002
+ }
5003
+ }
5004
+
4317
5005
  // src/commands/status.ts
4318
5006
  async function runStatus(repoRoot) {
4319
5007
  const status = await harnessStatus(repoRoot);
@@ -4338,8 +5026,8 @@ async function runStatus(repoRoot) {
4338
5026
  }
4339
5027
 
4340
5028
  // src/core/packs/index.ts
4341
- import { cp as cp3, mkdir as mkdir12, readFile as readFile12, readdir as readdir6, stat as stat10 } from "fs/promises";
4342
- import path27 from "path";
5029
+ import { cp as cp3, mkdir as mkdir12, readFile as readFile13, readdir as readdir6, stat as stat11 } from "fs/promises";
5030
+ import path29 from "path";
4343
5031
  import { fileURLToPath as fileURLToPath2 } from "url";
4344
5032
  import { parse as parseYaml3 } from "yaml";
4345
5033
  import { z as z5 } from "zod";
@@ -4352,18 +5040,18 @@ var packManifestSchema = z5.object({
4352
5040
  rules: z5.array(z5.string()).default([]),
4353
5041
  connections: z5.array(z5.string()).default([])
4354
5042
  });
4355
- var DIST_DIR2 = path27.dirname(fileURLToPath2(import.meta.url));
4356
- var PACKAGE_ROOT_FROM_BUNDLE2 = path27.resolve(DIST_DIR2, "..");
4357
- var PACKAGE_ROOT_FROM_DIST2 = path27.resolve(DIST_DIR2, "../../..");
4358
- var PACKAGE_ROOT_FROM_SRC2 = path27.resolve(DIST_DIR2, "../../../..");
5043
+ var DIST_DIR2 = path29.dirname(fileURLToPath2(import.meta.url));
5044
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path29.resolve(DIST_DIR2, "..");
5045
+ var PACKAGE_ROOT_FROM_DIST2 = path29.resolve(DIST_DIR2, "../../..");
5046
+ var PACKAGE_ROOT_FROM_SRC2 = path29.resolve(DIST_DIR2, "../../../..");
4359
5047
  var PACK_CANDIDATES = [
4360
- path27.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
4361
- path27.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
4362
- path27.join(PACKAGE_ROOT_FROM_SRC2, "packs")
5048
+ path29.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
5049
+ path29.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
5050
+ path29.join(PACKAGE_ROOT_FROM_SRC2, "packs")
4363
5051
  ];
4364
5052
  async function exists5(target) {
4365
5053
  try {
4366
- await stat10(target);
5054
+ await stat11(target);
4367
5055
  return true;
4368
5056
  } catch (error) {
4369
5057
  if (error.code === "ENOENT") {
@@ -4391,22 +5079,22 @@ async function isPackRoot(candidate) {
4391
5079
  return false;
4392
5080
  }
4393
5081
  for (const entry of entries) {
4394
- if (entry.isDirectory() && await exists5(path27.join(candidate, entry.name, "pack.yaml"))) {
5082
+ if (entry.isDirectory() && await exists5(path29.join(candidate, entry.name, "pack.yaml"))) {
4395
5083
  return true;
4396
5084
  }
4397
5085
  }
4398
5086
  return false;
4399
5087
  }
4400
5088
  function safeRelative(ref) {
4401
- const normalized = path27.normalize(ref);
4402
- if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
5089
+ const normalized = path29.normalize(ref);
5090
+ if (path29.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path29.sep}`)) {
4403
5091
  throw new Error(`Unsafe pack reference: ${ref}`);
4404
5092
  }
4405
5093
  return normalized;
4406
5094
  }
4407
5095
  async function readPackManifest(packDir) {
4408
- const file = path27.join(packDir, "pack.yaml");
4409
- const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
5096
+ const file = path29.join(packDir, "pack.yaml");
5097
+ const parsed = packManifestSchema.safeParse(parseYaml3(await readFile13(file, "utf8")));
4410
5098
  if (!parsed.success) {
4411
5099
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
4412
5100
  throw new Error(`Invalid pack manifest ${file}: ${detail}`);
@@ -4414,7 +5102,7 @@ async function readPackManifest(packDir) {
4414
5102
  return parsed.data;
4415
5103
  }
4416
5104
  async function packDirFor(repoRoot, nameOrPath) {
4417
- if (path27.isAbsolute(nameOrPath)) {
5105
+ if (path29.isAbsolute(nameOrPath)) {
4418
5106
  return nameOrPath;
4419
5107
  }
4420
5108
  if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
@@ -4422,14 +5110,14 @@ async function packDirFor(repoRoot, nameOrPath) {
4422
5110
  }
4423
5111
  const bundled = await bundledPacksDir();
4424
5112
  if (bundled) {
4425
- return path27.join(bundled, nameOrPath);
5113
+ return path29.join(bundled, nameOrPath);
4426
5114
  }
4427
- return toRepoPath(repoRoot, path27.join("packs", nameOrPath));
5115
+ return toRepoPath(repoRoot, path29.join("packs", nameOrPath));
4428
5116
  }
4429
5117
  async function directFiles(dir, ext) {
4430
5118
  try {
4431
5119
  const entries = await readdir6(dir, { withFileTypes: true });
4432
- return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path27.join(dir, entry.name)).sort();
5120
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path29.join(dir, entry.name)).sort();
4433
5121
  } catch (error) {
4434
5122
  if (error.code === "ENOENT") {
4435
5123
  return [];
@@ -4442,11 +5130,11 @@ async function skillEntries(dir) {
4442
5130
  const entries = await readdir6(dir, { withFileTypes: true });
4443
5131
  const result = [];
4444
5132
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4445
- const full = path27.join(dir, entry.name);
5133
+ const full = path29.join(dir, entry.name);
4446
5134
  if (entry.isFile() && entry.name.endsWith(".md")) {
4447
5135
  result.push(full);
4448
5136
  }
4449
- if (entry.isDirectory() && await exists5(path27.join(full, "SKILL.md"))) {
5137
+ if (entry.isDirectory() && await exists5(path29.join(full, "SKILL.md"))) {
4450
5138
  result.push(full);
4451
5139
  }
4452
5140
  }
@@ -4461,34 +5149,34 @@ async function skillEntries(dir) {
4461
5149
  async function collectObjects(packDir, manifest) {
4462
5150
  async function resolveRef(ref) {
4463
5151
  const safe = safeRelative(ref);
4464
- const local = path27.resolve(packDir, safe);
5152
+ const local = path29.resolve(packDir, safe);
4465
5153
  if (await exists5(local)) {
4466
5154
  return local;
4467
5155
  }
4468
- return path27.resolve(packDir, "..", "..", safe);
5156
+ return path29.resolve(packDir, "..", "..", safe);
4469
5157
  }
4470
5158
  return {
4471
5159
  skills: [
4472
5160
  ...await Promise.all(manifest.skills.map(resolveRef)),
4473
- ...await skillEntries(path27.join(packDir, "skills"))
5161
+ ...await skillEntries(path29.join(packDir, "skills"))
4474
5162
  ],
4475
5163
  tools: [
4476
5164
  ...await Promise.all(manifest.tools.map(resolveRef)),
4477
- ...await directFiles(path27.join(packDir, "tools"), ".yaml")
5165
+ ...await directFiles(path29.join(packDir, "tools"), ".yaml")
4478
5166
  ],
4479
5167
  rules: [
4480
5168
  ...await Promise.all(manifest.rules.map(resolveRef)),
4481
- ...await directFiles(path27.join(packDir, "rules"), ".md")
5169
+ ...await directFiles(path29.join(packDir, "rules"), ".md")
4482
5170
  ],
4483
5171
  connections: [
4484
5172
  ...await Promise.all(manifest.connections.map(resolveRef)),
4485
- ...await directFiles(path27.join(packDir, "connections"), ".yaml")
5173
+ ...await directFiles(path29.join(packDir, "connections"), ".yaml")
4486
5174
  ]
4487
5175
  };
4488
5176
  }
4489
5177
  function baseName(source) {
4490
- const parsed = path27.basename(source) === "SKILL.md" ? path27.dirname(source) : source;
4491
- return path27.basename(parsed, path27.extname(parsed));
5178
+ const parsed = path29.basename(source) === "SKILL.md" ? path29.dirname(source) : source;
5179
+ return path29.basename(parsed, path29.extname(parsed));
4492
5180
  }
4493
5181
  async function listPacks(repoRoot) {
4494
5182
  const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
@@ -4505,8 +5193,8 @@ async function listPacks(repoRoot) {
4505
5193
  if (!entry.isDirectory() || seen.has(entry.name)) {
4506
5194
  continue;
4507
5195
  }
4508
- const packDir = path27.join(root, entry.name);
4509
- if (!await exists5(path27.join(packDir, "pack.yaml"))) {
5196
+ const packDir = path29.join(root, entry.name);
5197
+ if (!await exists5(path29.join(packDir, "pack.yaml"))) {
4510
5198
  continue;
4511
5199
  }
4512
5200
  seen.add(entry.name);
@@ -4530,14 +5218,14 @@ async function inspectPack(repoRoot, nameOrPath) {
4530
5218
  };
4531
5219
  }
4532
5220
  async function validateProse(file, kind) {
4533
- const target = path27.basename(file) === "SKILL.md" ? file : file;
4534
- const content = await readFile12(target, "utf8");
5221
+ const target = path29.basename(file) === "SKILL.md" ? file : file;
5222
+ const content = await readFile13(target, "utf8");
4535
5223
  const parsed = parseFrontmatter(content);
4536
5224
  const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
4537
5225
  schema.parse(parsed.data);
4538
5226
  }
4539
5227
  async function validateYaml(file, kind) {
4540
- const content = await readFile12(file, "utf8");
5228
+ const content = await readFile13(file, "utf8");
4541
5229
  const schema = kind === "tool" ? toolManifestSchema : connectionManifestSchema;
4542
5230
  schema.parse(parseYaml3(content));
4543
5231
  }
@@ -4548,7 +5236,7 @@ async function validatePack(repoRoot, nameOrPath) {
4548
5236
  const manifest = await readPackManifest(packDir);
4549
5237
  const objects = await collectObjects(packDir, manifest);
4550
5238
  for (const skill of objects.skills) {
4551
- await validateProse(path27.basename(skill) === "SKILL.md" ? skill : path27.join(skill, "SKILL.md"), "skill");
5239
+ await validateProse(path29.basename(skill) === "SKILL.md" ? skill : path29.join(skill, "SKILL.md"), "skill");
4552
5240
  }
4553
5241
  for (const rule of objects.rules) await validateProse(rule, "rule");
4554
5242
  for (const tool of objects.tools) await validateYaml(tool, "tool");
@@ -4562,9 +5250,9 @@ async function validatePack(repoRoot, nameOrPath) {
4562
5250
  return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
4563
5251
  }
4564
5252
  async function copyObject(source, destDir) {
4565
- const info = await stat10(source);
5253
+ const info = await stat11(source);
4566
5254
  const name = baseName(source);
4567
- const dest = info.isDirectory() ? path27.join(destDir, name) : path27.join(destDir, path27.basename(source));
5255
+ const dest = info.isDirectory() ? path29.join(destDir, name) : path29.join(destDir, path29.basename(source));
4568
5256
  await mkdir12(destDir, { recursive: true });
4569
5257
  await cp3(source, dest, { recursive: true, force: true });
4570
5258
  return dest;
@@ -4853,7 +5541,9 @@ async function runConnectionsCheck(repoRoot) {
4853
5541
  // src/cli.ts
4854
5542
  function createProgram(repoRoot = process.cwd()) {
4855
5543
  const program = new Command();
4856
- program.name("threadroot").description("Git for your AI agent harness: one canonical .threadroot source, optional provider exposure.").version("0.1.1");
5544
+ program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version("0.1.3");
5545
+ program.command("bootstrap").description("Plan or apply first-run Threadroot setup for this machine and repository.").option("-y, --yes", "Apply the setup plan. Without --yes, bootstrap prints a dry-run plan.").option("--dry-run", "Print the setup plan without writing files.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--task <task>", "Task used for the initial context slice.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").option("--expose <list>", "Also write project provider skill shims: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--no-global", "Skip one-time machine-level agent setup.").option("--no-init", "Skip project harness initialization.").option("--no-import", "Skip importing existing vendor files during init.").option("--profile <profile>", "Override the detected project profile during init.").action((options) => runBootstrap(repoRoot, options));
5546
+ program.command("start").argument("[task]", "Task to prepare context for.").option("--task <task>", "Task to prepare context for.").description("Start a focused Threadroot agent session: doctor, status, context, and command map.").action((task, options) => runStart(repoRoot, task, options));
4857
5547
  program.command("init").description("Scaffold a local-only Threadroot harness and import existing vendor files once.").option("--force", "Re-initialize over an existing harness.").option("--no-import", "Skip importing existing vendor files (blank-slate init).").option("--profile <profile>", "Override the detected project profile.").option("--adapters <list>", "Comma-separated adapters: agents,claude,copilot,cursor.").option("--expose <list>", "Comma-separated provider skill shims to write: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").action((options) => runInit(repoRoot, options));
4858
5548
  program.command("expose").argument("[agent]", "Provider(s) to expose: codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show project files that would be written.").option("--check", "Check current project exposure state.").option("--undo", "Remove Threadroot-managed project exposure files.").option("--force", "Replace an existing unmanaged threadroot skill.").description("Write thin provider project skills that point agents at `.threadroot/`.").action((agent, options) => runExpose(repoRoot, agent, options));
4859
5549
  program.command("setup").option("--global", "Install machine-level Threadroot agent bootstrap skills/config.").option("--agent <list>", "Provider(s): codex,claude,cursor,copilot,gemini,windsurf,antigravity,opencode,all.").option("--dry-run", "Show global files that would be written.").option("--check", "Check global Threadroot setup state.").option("--undo", "Remove Threadroot-managed global setup files/blocks.").option("--force", "Replace an existing unmanaged threadroot skill.").option("--mcp", "Also add Threadroot MCP to Codex global config when Codex is selected.").description("Set up Threadroot once per machine for supported coding agents.").action((options) => runSetup(repoRoot, options));
@@ -4889,6 +5579,7 @@ function createProgram(repoRoot = process.cwd()) {
4889
5579
  skills.command("validate").option("--path <path>", "Validate a repo-relative skill file, skill directory, or skill collection.").description("Validate skill frontmatter, naming, trigger descriptions, and progressive-disclosure hygiene.").action((options) => runSkillsValidate(repoRoot, options));
4890
5580
  const mcp = program.command("mcp").description("Run or configure the local Threadroot MCP server.");
4891
5581
  mcp.action(() => runMcp(repoRoot));
5582
+ mcp.command("check").option("--timeout <ms>", "Handshake timeout in milliseconds.").description("Verify Codex MCP config and the Threadroot stdio server handshake.").action((options) => runMcpCheck(repoRoot, options));
4892
5583
  mcp.command("setup").option("--agent <agent>", "all, generic, codex, copilot, cursor, or claude.").option("--write", "Write project-local MCP config files for the agents.").description("Print MCP config snippets and a pasteable agent bootstrap prompt.").action((options) => runMcpSetup(repoRoot, options));
4893
5584
  return program;
4894
5585
  }