threadroot 0.1.1 → 0.1.2

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 +13 -0
  2. package/README.md +23 -23
  3. package/dist/index.js +1462 -1212
  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 path22 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 path14 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(
@@ -872,7 +413,7 @@ async function compile(repoRoot, harness) {
872
413
  async function detectDrift(repoRoot, files) {
873
414
  const entries = await Promise.all(
874
415
  files.map(async (file) => {
875
- const existing = await readIfExists(path5.join(repoRoot, file.path));
416
+ const existing = await readIfExists(path2.join(repoRoot, file.path));
876
417
  if (existing === void 0) {
877
418
  return { path: file.path, status: "create" };
878
419
  }
@@ -883,176 +424,480 @@ async function detectDrift(repoRoot, files) {
883
424
  return entries;
884
425
  }
885
426
 
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);
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);
896
553
  }
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 };
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);
904
571
  }
905
572
 
906
- // src/commands/compile.ts
907
- async function runCompileCommand(repoRoot, options) {
908
- const adapter = options.adapter ? adapterIdSchema.parse(options.adapter) : void 0;
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;
909
608
  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}`);
609
+ entries = await readdir(dir);
610
+ } catch (error) {
611
+ if (error.code === "ENOENT") {
612
+ return [];
915
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 });
916
628
  } catch (error) {
917
- if (error instanceof HarnessError) {
918
- console.error(error.message);
919
- process.exitCode = 1;
920
- return;
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
+ }
649
+ }
650
+ }
651
+ }
652
+ return files;
653
+ }
654
+ function objectDirFor(repoRoot, dir, origin, home) {
655
+ return origin === "project" ? projectObjectDir(repoRoot, dir) : userObjectDir(dir, home);
656
+ }
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);
663
+ }
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)}`);
921
671
  }
922
- throw error;
923
- }
672
+ return { name: result.data.name, origin, sourcePath: file.path, frontmatter: result.data, body };
673
+ });
924
674
  }
925
-
926
- // src/commands/context.ts
927
- async function runContext(repoRoot, task) {
928
- let context;
929
- try {
930
- context = await assembleContext(repoRoot, task);
931
- } catch (error) {
932
- if (error instanceof HarnessError) {
933
- console.log("No harness found. Run `tr init` first.");
934
- return;
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)}`);
935
682
  }
936
- throw error;
937
- }
938
- console.log(`task: ${context.task}`);
939
- if (context.skills.length > 0) {
940
- console.log("\nskills:");
941
- for (const skill of context.skills) {
942
- console.log(`- ${skill.name} - ${skill.when}`);
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)}`);
943
693
  }
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})` : ""}`);
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)}`);
949
704
  }
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}`);
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;
955
716
  }
717
+ const { body } = parseFrontmatter(file.content);
718
+ memory.push({ type: type.data, origin, sourcePath: file.path, body });
956
719
  }
957
- if (context.memory.length > 0) {
958
- console.log("\nmemory:");
959
- for (const entry of context.memory) {
960
- console.log(`- ${entry.type}`);
961
- }
720
+ return memory;
721
+ }
722
+ function mergeByName(user, project) {
723
+ const merged = /* @__PURE__ */ new Map();
724
+ for (const item of user) {
725
+ merged.set(item.name, item);
962
726
  }
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.");
727
+ for (const item of project) {
728
+ merged.set(item.name, item);
965
729
  }
730
+ return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
966
731
  }
967
-
968
- // src/commands/diff.ts
969
- import fs from "fs/promises";
970
- import path7 from "path";
971
- async function readIfExists2(filePath) {
732
+ async function loadManifest(repoRoot) {
733
+ const manifestPath = projectManifestPath(repoRoot);
734
+ let raw;
972
735
  try {
973
- return await fs.readFile(filePath, "utf8");
736
+ raw = await readFile2(manifestPath, "utf8");
974
737
  } catch (error) {
975
738
  if (error.code === "ENOENT") {
976
- return void 0;
739
+ throw new HarnessError(`No harness found at ${manifestPath}. Run \`tr init\` first.`);
977
740
  }
978
741
  throw error;
979
742
  }
980
- }
981
- function lineDiff(before, after) {
982
- const a = before.length === 0 ? [] : before.split("\n");
983
- const b = after.length === 0 ? [] : after.split("\n");
984
- const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
985
- for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
986
- for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
987
- lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
988
- }
989
- }
990
- const lines = [];
991
- let i = 0;
992
- let j = 0;
993
- while (i < a.length && j < b.length) {
994
- if (a[i] === b[j]) {
995
- i += 1;
996
- j += 1;
997
- } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
998
- lines.push(`- ${a[i]}`);
999
- i += 1;
1000
- } else {
1001
- lines.push(`+ ${b[j]}`);
1002
- j += 1;
1003
- }
743
+ const result = harnessManifestSchema.safeParse(parseYaml2(raw));
744
+ if (!result.success) {
745
+ throw new HarnessError(`Invalid ${manifestPath}: ${describe(result.error)}`);
1004
746
  }
1005
- while (i < a.length) {
1006
- lines.push(`- ${a[i]}`);
1007
- i += 1;
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
+ }));
1008
820
  }
1009
- while (j < b.length) {
1010
- lines.push(`+ ${b[j]}`);
1011
- j += 1;
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
+ );
1012
847
  }
1013
- return lines;
848
+ return parsed.data;
1014
849
  }
1015
- async function runDiff(repoRoot) {
1016
- let harness;
850
+ function memoryDir(repoRoot, scope, home) {
851
+ return scope === "project" ? projectObjectDir(repoRoot, "memory") : userObjectDir("memory", home);
852
+ }
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);
1017
859
  try {
1018
- harness = await resolveHarness(repoRoot);
860
+ const raw = await readFile3(file, "utf8");
861
+ return parseFrontmatter(raw).body;
1019
862
  } catch (error) {
1020
- if (error instanceof HarnessError) {
1021
- console.log("No harness found. Run `tr init` first.");
1022
- return;
863
+ if (error.code === "ENOENT") {
864
+ return null;
1023
865
  }
1024
866
  throw error;
1025
867
  }
1026
- const files = await compile(repoRoot, harness);
1027
- let changed = 0;
1028
- for (const file of files) {
1029
- const existing = await readIfExists2(path7.join(repoRoot, file.path));
1030
- if (existing === void 0) {
1031
- changed += 1;
1032
- console.log(`+ ${file.path} (new)`);
1033
- continue;
1034
- }
1035
- if (existing === file.content) {
1036
- continue;
1037
- }
1038
- changed += 1;
1039
- console.log(`~ ${file.path}`);
1040
- for (const line of lineDiff(existing, file.content)) {
1041
- console.log(` ${line}`);
1042
- }
1043
- }
1044
- if (changed === 0) {
1045
- console.log("No drift: optional compiled outputs match the canonical harness.");
868
+ }
869
+ function headingFor(type) {
870
+ const title = type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
871
+ return `# ${title}`;
872
+ }
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 = "";
883
+ try {
884
+ existing = await readFile3(file, "utf8");
885
+ } catch (error) {
886
+ if (error.code !== "ENOENT") {
887
+ throw error;
888
+ }
1046
889
  }
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,7 +1792,8 @@ 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
  ""
@@ -1994,7 +1841,7 @@ async function setupGlobalSkill(home, provider, mode, force) {
1994
1841
  message: "Existing skill is not Threadroot-managed."
1995
1842
  };
1996
1843
  }
1997
- await rm(path13.dirname(filePath), { recursive: true, force: true });
1844
+ await rm(path11.dirname(filePath), { recursive: true, force: true });
1998
1845
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status: "removed" };
1999
1846
  }
2000
1847
  if (existing === desired) {
@@ -2014,8 +1861,8 @@ async function setupGlobalSkill(home, provider, mode, force) {
2014
1861
  if (mode === "dry-run") {
2015
1862
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2016
1863
  }
2017
- await mkdir5(path13.dirname(filePath), { recursive: true });
2018
- await writeFile5(filePath, desired, "utf8");
1864
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1865
+ await writeFile4(filePath, desired, "utf8");
2019
1866
  return { kind: "skill", agent: provider.id, label: provider.label, path: shown, status };
2020
1867
  }
2021
1868
  async function setupCodexAgents(home, mode) {
@@ -2036,7 +1883,7 @@ async function setupCodexAgents(home, mode) {
2036
1883
  if (!hasManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END)) {
2037
1884
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "missing" };
2038
1885
  }
2039
- await writeFile5(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
1886
+ await writeFile4(filePath, removeManagedBlock(existing, CODEX_AGENTS_BEGIN, CODEX_AGENTS_END), "utf8");
2040
1887
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status: "removed" };
2041
1888
  }
2042
1889
  if (existing === desired) {
@@ -2046,8 +1893,8 @@ async function setupCodexAgents(home, mode) {
2046
1893
  if (mode === "dry-run") {
2047
1894
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2048
1895
  }
2049
- await mkdir5(path13.dirname(filePath), { recursive: true });
2050
- await writeFile5(filePath, desired, "utf8");
1896
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1897
+ await writeFile4(filePath, desired, "utf8");
2051
1898
  return { kind: "codex-agents", agent: "codex", label: "Codex global AGENTS.md", path: shown, status };
2052
1899
  }
2053
1900
  async function setupCodexMcp(home, mode) {
@@ -2078,7 +1925,7 @@ async function setupCodexMcp(home, mode) {
2078
1925
  if (!hasManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END)) {
2079
1926
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "missing" };
2080
1927
  }
2081
- await writeFile5(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
1928
+ await writeFile4(filePath, removeManagedBlock(existing, CODEX_MCP_BEGIN, CODEX_MCP_END), "utf8");
2082
1929
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status: "removed" };
2083
1930
  }
2084
1931
  if (existing === desired) {
@@ -2088,8 +1935,8 @@ async function setupCodexMcp(home, mode) {
2088
1935
  if (mode === "dry-run") {
2089
1936
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2090
1937
  }
2091
- await mkdir5(path13.dirname(filePath), { recursive: true });
2092
- await writeFile5(filePath, desired, "utf8");
1938
+ await mkdir4(path11.dirname(filePath), { recursive: true });
1939
+ await writeFile4(filePath, desired, "utf8");
2093
1940
  return { kind: "codex-mcp", agent: "codex", label: "Codex MCP config", path: shown, status };
2094
1941
  }
2095
1942
  async function setupGlobal(options = {}) {
@@ -2235,8 +2082,8 @@ function inputEnv(values) {
2235
2082
  }
2236
2083
 
2237
2084
  // src/core/tools/create.ts
2238
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
2239
- import path14 from "path";
2085
+ import { mkdir as mkdir5, writeFile as writeFile5 } from "fs/promises";
2086
+ import path12 from "path";
2240
2087
  import { stringify as stringifyYaml3 } from "yaml";
2241
2088
  var ToolCreateError = class extends Error {
2242
2089
  constructor(message) {
@@ -2253,7 +2100,7 @@ function assertSafeName(name) {
2253
2100
  }
2254
2101
  }
2255
2102
  function assertSafeScript(script) {
2256
- if (path14.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
2103
+ if (path12.isAbsolute(script) || script.split(/[\\/]/).includes("..")) {
2257
2104
  throw new ToolCreateError(`Script path must be inside the harness directory: ${script}`);
2258
2105
  }
2259
2106
  }
@@ -2282,9 +2129,9 @@ async function createTool(repoRoot, input2, options) {
2282
2129
  throw new ToolCreateError(`Invalid tool definition: ${detail}`);
2283
2130
  }
2284
2131
  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(
2132
+ const filePath = path12.join(dir, `${input2.name}.yaml`);
2133
+ await mkdir5(dir, { recursive: true });
2134
+ await writeFile5(filePath, stringifyYaml3(parsed.data), { encoding: "utf8", flag: options.force ? "w" : "wx" }).catch(
2288
2135
  (error) => {
2289
2136
  if (error.code === "EEXIST") {
2290
2137
  throw new ToolCreateError(`Tool \`${input2.name}\` already exists at ${filePath}. Pass force to overwrite.`);
@@ -2297,7 +2144,7 @@ async function createTool(repoRoot, input2, options) {
2297
2144
 
2298
2145
  // src/core/tools/catalog.ts
2299
2146
  import { readFile as readFile7, stat as stat4 } from "fs/promises";
2300
- import path15 from "path";
2147
+ import path13 from "path";
2301
2148
  var NAME_RE3 = /^[a-z0-9][a-z0-9-]*$/;
2302
2149
  var DESTRUCTIVE_RE = /\b(migrate|deploy|publish|release|prune|reset|destroy|drop|delete|rm|push|seed|wipe)\b/i;
2303
2150
  function looksDestructive(name, command) {
@@ -2316,13 +2163,13 @@ async function exists2(file) {
2316
2163
  }
2317
2164
  }
2318
2165
  async function detectPackageManager(repoRoot) {
2319
- if (await exists2(path15.join(repoRoot, "pnpm-lock.yaml"))) {
2166
+ if (await exists2(path13.join(repoRoot, "pnpm-lock.yaml"))) {
2320
2167
  return "pnpm";
2321
2168
  }
2322
- if (await exists2(path15.join(repoRoot, "yarn.lock"))) {
2169
+ if (await exists2(path13.join(repoRoot, "yarn.lock"))) {
2323
2170
  return "yarn";
2324
2171
  }
2325
- if (await exists2(path15.join(repoRoot, "bun.lockb"))) {
2172
+ if (await exists2(path13.join(repoRoot, "bun.lockb"))) {
2326
2173
  return "bun";
2327
2174
  }
2328
2175
  return "npm";
@@ -2343,7 +2190,7 @@ function orderScripts(names) {
2343
2190
  });
2344
2191
  }
2345
2192
  async function fromPackageJson(repoRoot) {
2346
- const file = path15.join(repoRoot, "package.json");
2193
+ const file = path13.join(repoRoot, "package.json");
2347
2194
  let raw;
2348
2195
  try {
2349
2196
  raw = await readFile7(file, "utf8");
@@ -2380,7 +2227,7 @@ async function fromPackageJson(repoRoot) {
2380
2227
  async function fromTargets(repoRoot, fileName, runner, source) {
2381
2228
  let raw;
2382
2229
  try {
2383
- raw = await readFile7(path15.join(repoRoot, fileName), "utf8");
2230
+ raw = await readFile7(path13.join(repoRoot, fileName), "utf8");
2384
2231
  } catch {
2385
2232
  return [];
2386
2233
  }
@@ -2560,7 +2407,7 @@ function finding2(severity, code, message, pathValue) {
2560
2407
  }
2561
2408
  async function mcpConfigHints(repoRoot) {
2562
2409
  const configs = [".vscode/mcp.json", ".cursor/mcp.json", ".mcp.json"];
2563
- const present = await Promise.all(configs.map((config) => exists3(path16.join(repoRoot, config))));
2410
+ const present = await Promise.all(configs.map((config) => exists3(path14.join(repoRoot, config))));
2564
2411
  if (present.some(Boolean)) {
2565
2412
  return [];
2566
2413
  }
@@ -2580,7 +2427,7 @@ async function globalSetupHints(home) {
2580
2427
  finding2(
2581
2428
  "info",
2582
2429
  "global_setup_missing",
2583
- "Codex global Threadroot setup was not detected. Run `threadroot setup --global --agent codex` for one-time machine setup."
2430
+ "Codex global Threadroot setup was not detected. Run `threadroot bootstrap --yes --agent codex` for one-time machine setup."
2584
2431
  )
2585
2432
  ];
2586
2433
  }
@@ -2734,7 +2581,7 @@ async function doctor(repoRoot, options = {}) {
2734
2581
  )
2735
2582
  );
2736
2583
  }
2737
- const scriptsDir = path16.join(path16.dirname(skill.sourcePath), "scripts");
2584
+ const scriptsDir = path14.join(path14.dirname(skill.sourcePath), "scripts");
2738
2585
  if (await exists3(scriptsDir)) {
2739
2586
  findings.push(
2740
2587
  finding2(
@@ -2757,35 +2604,9 @@ function summarize(findings) {
2757
2604
  return { ok: errors === 0, findings, summary: { errors, warnings, info } };
2758
2605
  }
2759
2606
 
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
2607
  // 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";
2608
+ import { mkdir as mkdir6, readFile as readFile8, rm as rm2, stat as stat6, writeFile as writeFile6 } from "fs/promises";
2609
+ import path15 from "path";
2789
2610
  async function readMaybe2(filePath) {
2790
2611
  try {
2791
2612
  return await readFile8(filePath, "utf8");
@@ -2797,10 +2618,10 @@ async function readMaybe2(filePath) {
2797
2618
  }
2798
2619
  }
2799
2620
  function projectSkillPath(repoRoot, provider) {
2800
- return path17.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2621
+ return path15.join(repoRoot, provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2801
2622
  }
2802
2623
  function relSkillPath(provider) {
2803
- return path17.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2624
+ return path15.join(provider.projectSkillDir, THREADROOT_SKILL_NAME, "SKILL.md");
2804
2625
  }
2805
2626
  async function exposeOne(repoRoot, provider, mode, force) {
2806
2627
  const relativePath = relSkillPath(provider);
@@ -2832,7 +2653,7 @@ async function exposeOne(repoRoot, provider, mode, force) {
2832
2653
  message: "Existing skill is not Threadroot-managed."
2833
2654
  };
2834
2655
  }
2835
- await rm2(path17.dirname(absolutePath), { recursive: true, force: true });
2656
+ await rm2(path15.dirname(absolutePath), { recursive: true, force: true });
2836
2657
  return { agent: provider.id, label: provider.label, path: relativePath, status: "removed" };
2837
2658
  }
2838
2659
  if (existing === desired) {
@@ -2851,8 +2672,8 @@ async function exposeOne(repoRoot, provider, mode, force) {
2851
2672
  if (mode === "dry-run") {
2852
2673
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2853
2674
  }
2854
- await mkdir7(path17.dirname(absolutePath), { recursive: true });
2855
- await writeFile7(absolutePath, desired, "utf8");
2675
+ await mkdir6(path15.dirname(absolutePath), { recursive: true });
2676
+ await writeFile6(absolutePath, desired, "utf8");
2856
2677
  return { agent: provider.id, label: provider.label, path: relativePath, status };
2857
2678
  }
2858
2679
  async function exposeProject(repoRoot, options = {}) {
@@ -2865,35 +2686,35 @@ async function exposeProject(repoRoot, options = {}) {
2865
2686
  return { entries };
2866
2687
  }
2867
2688
 
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
2689
  // src/core/init/index.ts
2891
2690
  import { mkdir as mkdir9, stat as stat8, writeFile as writeFile8 } from "fs/promises";
2892
- import path22 from "path";
2691
+ import path21 from "path";
2692
+
2693
+ // src/core/compile/write.ts
2694
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
2695
+ import path16 from "path";
2696
+ async function writeCompiled(repoRoot, files) {
2697
+ await Promise.all(
2698
+ files.map(async (file) => {
2699
+ const absolute = path16.join(repoRoot, file.path);
2700
+ await mkdir7(path16.dirname(absolute), { recursive: true });
2701
+ await writeFile7(absolute, file.content, "utf8");
2702
+ })
2703
+ );
2704
+ return files.map((file) => file.path);
2705
+ }
2706
+ async function runCompile(repoRoot, options = {}) {
2707
+ const resolved = options.harness ?? await resolveHarness(repoRoot, { home: options.home });
2708
+ const harness = options.adapter ? { ...resolved, manifest: { ...resolved.manifest, adapters: [options.adapter] } } : resolved;
2709
+ const files = await compile(repoRoot, harness);
2710
+ const drift = await detectDrift(repoRoot, files);
2711
+ const written = await writeCompiled(repoRoot, files);
2712
+ return { written, drift };
2713
+ }
2893
2714
 
2894
2715
  // src/core/scan/package.ts
2895
- import fs2 from "fs/promises";
2896
- import path18 from "path";
2716
+ import fs from "fs/promises";
2717
+ import path17 from "path";
2897
2718
 
2898
2719
  // src/core/scan/rules.ts
2899
2720
  var ignoredDirectories = /* @__PURE__ */ new Set([
@@ -2911,13 +2732,13 @@ var ignoredDirectories = /* @__PURE__ */ new Set([
2911
2732
  // src/core/scan/package.ts
2912
2733
  async function readJson(repoRoot, relativePath) {
2913
2734
  try {
2914
- return JSON.parse(await fs2.readFile(path18.join(repoRoot, relativePath), "utf8"));
2735
+ return JSON.parse(await fs.readFile(path17.join(repoRoot, relativePath), "utf8"));
2915
2736
  } catch {
2916
2737
  return void 0;
2917
2738
  }
2918
2739
  }
2919
2740
  function inferProfile(files, packageJson) {
2920
- if (files.some((file) => path18.basename(file) === "dbt_project.yml" || path18.basename(file) === "dbt_project.yaml")) {
2741
+ if (files.some((file) => path17.basename(file) === "dbt_project.yml" || path17.basename(file) === "dbt_project.yaml")) {
2921
2742
  return "dbt";
2922
2743
  }
2923
2744
  const packageMeta = packageJson && typeof packageJson === "object" ? packageJson : void 0;
@@ -2941,385 +2762,837 @@ function inferProfile(files, packageJson) {
2941
2762
  }
2942
2763
 
2943
2764
  // src/core/scan/walk.ts
2944
- import fs3 from "fs/promises";
2945
- import path19 from "path";
2765
+ import fs2 from "fs/promises";
2766
+ import path18 from "path";
2946
2767
  function toPosix(relativePath) {
2947
- return relativePath.split(path19.sep).join("/");
2768
+ return relativePath.split(path18.sep).join("/");
2769
+ }
2770
+ async function walkRepo(repoRoot, directory = repoRoot) {
2771
+ let entries;
2772
+ try {
2773
+ entries = await fs2.readdir(directory, { withFileTypes: true });
2774
+ } catch {
2775
+ return [];
2776
+ }
2777
+ const files = [];
2778
+ for (const entry of entries) {
2779
+ const absolutePath = path18.join(directory, entry.name);
2780
+ const relativePath = toPosix(path18.relative(repoRoot, absolutePath));
2781
+ if (entry.isDirectory()) {
2782
+ if (!ignoredDirectories.has(entry.name)) {
2783
+ files.push(...await walkRepo(repoRoot, absolutePath));
2784
+ }
2785
+ continue;
2786
+ }
2787
+ if (entry.isFile()) {
2788
+ files.push(relativePath);
2789
+ }
2790
+ }
2791
+ return files;
2792
+ }
2793
+
2794
+ // src/core/init/index.ts
2795
+ import { stringify as stringifyYaml4 } from "yaml";
2796
+
2797
+ // src/core/init/builtins.ts
2798
+ import { cp, mkdir as mkdir8, readdir as readdir3, stat as stat7 } from "fs/promises";
2799
+ import path19 from "path";
2800
+ import { fileURLToPath } from "url";
2801
+ var DIST_DIR = path19.dirname(fileURLToPath(import.meta.url));
2802
+ var PACKAGE_ROOT_FROM_BUNDLE = path19.resolve(DIST_DIR, "..");
2803
+ var PACKAGE_ROOT_FROM_DIST = path19.resolve(DIST_DIR, "../../..");
2804
+ var PACKAGE_ROOT_FROM_SRC = path19.resolve(DIST_DIR, "../../../..");
2805
+ var SKILL_PACK_CANDIDATES = [
2806
+ path19.join(PACKAGE_ROOT_FROM_BUNDLE, "skills"),
2807
+ path19.join(PACKAGE_ROOT_FROM_DIST, "skills"),
2808
+ path19.join(PACKAGE_ROOT_FROM_SRC, "skills")
2809
+ ];
2810
+ var PROJECT_MEMORY_TEMPLATE = [
2811
+ "# Project",
2812
+ "",
2813
+ "<!-- Stable, rarely-changing facts about this project. Keep it short. -->",
2814
+ "",
2815
+ "- What it is:",
2816
+ "- Key technologies:",
2817
+ "- How to run it:"
2818
+ ].join("\n");
2819
+ async function exists4(target) {
2820
+ try {
2821
+ const info = await stat7(target);
2822
+ return info.isDirectory();
2823
+ } catch {
2824
+ return false;
2825
+ }
2826
+ }
2827
+ async function bundledSkillsDir() {
2828
+ for (const candidate of SKILL_PACK_CANDIDATES) {
2829
+ if (await exists4(candidate)) {
2830
+ return candidate;
2831
+ }
2832
+ }
2833
+ throw new Error("Bundled Threadroot skills were not found in this package.");
2834
+ }
2835
+ async function writeBuiltinSkills(repoRoot) {
2836
+ const sourceDir = await bundledSkillsDir();
2837
+ const targetDir = projectObjectDir(repoRoot, "skills");
2838
+ await mkdir8(targetDir, { recursive: true });
2839
+ const written = [];
2840
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
2841
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
2842
+ if (!entry.isDirectory()) {
2843
+ continue;
2844
+ }
2845
+ const sourceSkill = path19.join(sourceDir, entry.name);
2846
+ const sourceSkillFile = path19.join(sourceSkill, "SKILL.md");
2847
+ if (!await exists4(sourceSkill) || !await stat7(sourceSkillFile).then((info) => info.isFile()).catch(() => false)) {
2848
+ continue;
2849
+ }
2850
+ const targetSkill = path19.join(targetDir, entry.name);
2851
+ const targetSkillFile = path19.join(targetSkill, "SKILL.md");
2852
+ try {
2853
+ await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
2854
+ written.push(targetSkillFile);
2855
+ } catch (error) {
2856
+ if (error.code !== "ERR_FS_CP_EEXIST" && error.code !== "EEXIST") {
2857
+ throw error;
2858
+ }
2859
+ }
2860
+ }
2861
+ return written;
2862
+ }
2863
+
2864
+ // src/core/init/import.ts
2865
+ import { readFile as readFile9, readdir as readdir4 } from "fs/promises";
2866
+ import path20 from "path";
2867
+ var PROSE_PRECEDENCE = ["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"];
2868
+ var CURSOR_RULES_DIR = ".cursor/rules";
2869
+ var NAME_RE4 = /^[a-z0-9][a-z0-9-]*$/;
2870
+ async function readIfExists2(filePath) {
2871
+ try {
2872
+ return await readFile9(filePath, "utf8");
2873
+ } catch (error) {
2874
+ if (error.code === "ENOENT") {
2875
+ return void 0;
2876
+ }
2877
+ throw error;
2878
+ }
2879
+ }
2880
+ function normalize(text) {
2881
+ return text.toLowerCase().replace(/\s+/g, " ").trim();
2882
+ }
2883
+ function splitSections(markdown) {
2884
+ const sections = [];
2885
+ let heading = "";
2886
+ let buffer = [];
2887
+ const flush = () => {
2888
+ const text = buffer.join("\n").trim();
2889
+ if (text) {
2890
+ sections.push({ heading, text });
2891
+ }
2892
+ };
2893
+ for (const line of markdown.split(/\r?\n/)) {
2894
+ if (/^#{1,6}\s/.test(line)) {
2895
+ flush();
2896
+ heading = normalize(line.replace(/^#+\s*/, ""));
2897
+ buffer = [line];
2898
+ } else {
2899
+ buffer.push(line);
2900
+ }
2901
+ }
2902
+ flush();
2903
+ return sections;
2904
+ }
2905
+ function novelSections(canonical, other) {
2906
+ const haystack = normalize(canonical);
2907
+ const seenHeadings = new Set(splitSections(canonical).map((section) => section.heading).filter(Boolean));
2908
+ return splitSections(other).filter((section) => {
2909
+ if (section.heading && seenHeadings.has(section.heading)) {
2910
+ return false;
2911
+ }
2912
+ return !haystack.includes(normalize(section.text));
2913
+ });
2948
2914
  }
2949
- async function walkRepo(repoRoot, directory = repoRoot) {
2915
+ async function listCursorRules(repoRoot) {
2916
+ const dir = path20.join(repoRoot, CURSOR_RULES_DIR);
2950
2917
  let entries;
2951
2918
  try {
2952
- entries = await fs3.readdir(directory, { withFileTypes: true });
2953
- } catch {
2954
- return [];
2919
+ entries = await readdir4(dir);
2920
+ } catch (error) {
2921
+ if (error.code === "ENOENT") {
2922
+ return [];
2923
+ }
2924
+ throw error;
2955
2925
  }
2956
- const files = [];
2957
- for (const entry of entries) {
2958
- const absolutePath = path19.join(directory, entry.name);
2959
- const relativePath = toPosix(path19.relative(repoRoot, absolutePath));
2960
- if (entry.isDirectory()) {
2961
- if (!ignoredDirectories.has(entry.name)) {
2962
- files.push(...await walkRepo(repoRoot, absolutePath));
2963
- }
2926
+ const files = entries.filter((name) => name.endsWith(".mdc")).sort();
2927
+ return Promise.all(
2928
+ files.map(async (name) => ({
2929
+ file: `${CURSOR_RULES_DIR}/${name}`,
2930
+ content: await readFile9(path20.join(dir, name), "utf8")
2931
+ }))
2932
+ );
2933
+ }
2934
+ function globsToApplyTo(value) {
2935
+ if (typeof value === "string") {
2936
+ const first = value.split(",")[0]?.trim();
2937
+ return first || void 0;
2938
+ }
2939
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
2940
+ return value[0].trim() || void 0;
2941
+ }
2942
+ return void 0;
2943
+ }
2944
+ function ruleName(fileName) {
2945
+ const base = path20.basename(fileName, ".mdc").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2946
+ return NAME_RE4.test(base) ? base : "imported-rule";
2947
+ }
2948
+ async function importVendorFiles(repoRoot, options = {}) {
2949
+ const include = options.include ? new Set(options.include) : void 0;
2950
+ const wanted = (file) => !include || include.has(file);
2951
+ const prose = [];
2952
+ for (const file of PROSE_PRECEDENCE) {
2953
+ if (!wanted(file)) {
2964
2954
  continue;
2965
2955
  }
2966
- if (entry.isFile()) {
2967
- files.push(relativePath);
2956
+ const content = await readIfExists2(path20.join(repoRoot, file));
2957
+ if (content && content.trim()) {
2958
+ prose.push({ file, content });
2968
2959
  }
2969
2960
  }
2970
- return files;
2961
+ const cursorRules = (await listCursorRules(repoRoot)).filter((rule) => wanted(rule.file));
2962
+ let canonicalSource;
2963
+ let canonicalBody = "";
2964
+ let rest = [];
2965
+ if (prose.length > 0) {
2966
+ canonicalSource = prose[0].file;
2967
+ canonicalBody = extractHandAuthored(prose[0].content);
2968
+ rest = prose.slice(1);
2969
+ } else if (cursorRules.length > 0) {
2970
+ canonicalSource = CURSOR_RULES_DIR;
2971
+ canonicalBody = cursorRules.map((rule) => parseFrontmatter(rule.content).body).join("\n\n").trim();
2972
+ }
2973
+ const foldedFrom = [];
2974
+ const skippedDuplicates = [];
2975
+ let body = canonicalBody;
2976
+ for (const file of rest) {
2977
+ const hand = extractHandAuthored(file.content);
2978
+ const novel = novelSections(body, hand);
2979
+ if (novel.length === 0) {
2980
+ skippedDuplicates.push(file.file);
2981
+ continue;
2982
+ }
2983
+ body = `${body}
2984
+
2985
+ <!-- imported from ${file.file} -->
2986
+ ${novel.map((section) => section.text).join("\n\n")}`.trim();
2987
+ foldedFrom.push(file.file);
2988
+ }
2989
+ const importedRules = cursorRules.map((rule) => {
2990
+ const { data, body: ruleBody2 } = parseFrontmatter(rule.content);
2991
+ return {
2992
+ name: ruleName(rule.file),
2993
+ applyTo: globsToApplyTo(data.globs ?? data.applyTo),
2994
+ body: ruleBody2.trim()
2995
+ };
2996
+ });
2997
+ return { canonicalSource, canonicalBody: body, foldedFrom, importedRules, skippedDuplicates };
2971
2998
  }
2972
2999
 
2973
3000
  // 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) {
3001
+ var DEFAULT_ADAPTERS = [];
3002
+ var AGENTS_FILE2 = "AGENTS.md";
3003
+ var InitError = class extends Error {
3004
+ constructor(message) {
3005
+ super(message);
3006
+ this.name = "InitError";
3007
+ }
3008
+ };
3009
+ async function pathExists(target) {
2999
3010
  try {
3000
- const info = await stat7(target);
3001
- return info.isDirectory();
3011
+ await stat8(target);
3012
+ return true;
3002
3013
  } catch {
3003
3014
  return false;
3004
3015
  }
3005
3016
  }
3006
- async function bundledSkillsDir() {
3007
- for (const candidate of SKILL_PACK_CANDIDATES) {
3008
- if (await exists4(candidate)) {
3009
- return candidate;
3017
+ async function detectProfile(repoRoot, override) {
3018
+ if (override) {
3019
+ return override;
3020
+ }
3021
+ const files = await walkRepo(repoRoot);
3022
+ const packageJson = await readJson(repoRoot, "package.json");
3023
+ const inferred = inferProfile(files, packageJson);
3024
+ return inferred === "unknown" ? "empty" : inferred;
3025
+ }
3026
+ async function detectName(repoRoot) {
3027
+ const packageJson = await readJson(repoRoot, "package.json");
3028
+ if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) {
3029
+ return packageJson.name.trim();
3030
+ }
3031
+ return path21.basename(repoRoot);
3032
+ }
3033
+ async function writeManifest(repoRoot, manifest) {
3034
+ const body = {
3035
+ name: manifest.name,
3036
+ version: manifest.version,
3037
+ profile: manifest.profile,
3038
+ adapters: manifest.adapters
3039
+ };
3040
+ if (manifest.tools.allow.length > 0) {
3041
+ body.tools = { allow: manifest.tools.allow };
3042
+ }
3043
+ await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3044
+ await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3045
+ }
3046
+ async function writeProjectMemory(repoRoot) {
3047
+ const dir = projectObjectDir(repoRoot, "memory");
3048
+ await mkdir9(dir, { recursive: true });
3049
+ const filePath = path21.join(dir, "project.md");
3050
+ try {
3051
+ await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
3052
+ `, { encoding: "utf8", flag: "wx" });
3053
+ return [filePath];
3054
+ } catch (error) {
3055
+ if (error.code === "EEXIST") {
3056
+ return [];
3010
3057
  }
3058
+ throw error;
3011
3059
  }
3012
- throw new Error("Bundled Threadroot skills were not found in this package.");
3013
3060
  }
3014
- async function writeBuiltinSkills(repoRoot) {
3015
- const sourceDir = await bundledSkillsDir();
3016
- const targetDir = projectObjectDir(repoRoot, "skills");
3017
- await mkdir8(targetDir, { recursive: true });
3061
+ async function writeImportedRules(repoRoot, report) {
3062
+ if (report.importedRules.length === 0) {
3063
+ return [];
3064
+ }
3065
+ const dir = projectObjectDir(repoRoot, "rules");
3066
+ await mkdir9(dir, { recursive: true });
3018
3067
  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;
3068
+ for (const rule of report.importedRules) {
3069
+ const filePath = path21.join(dir, `${rule.name}.md`);
3070
+ const data = { name: rule.name, scope: "project" };
3071
+ if (rule.applyTo) {
3072
+ data.applyTo = rule.applyTo;
3028
3073
  }
3029
- const targetSkill = path20.join(targetDir, entry.name);
3030
- const targetSkillFile = path20.join(targetSkill, "SKILL.md");
3031
3074
  try {
3032
- await cp(sourceSkill, targetSkill, { recursive: true, force: false, errorOnExist: true });
3033
- written.push(targetSkillFile);
3075
+ await writeFile8(filePath, serializeFrontmatter(data, rule.body), { encoding: "utf8", flag: "wx" });
3076
+ written.push(filePath);
3034
3077
  } catch (error) {
3035
- if (error.code !== "ERR_FS_CP_EEXIST" && error.code !== "EEXIST") {
3078
+ if (error.code !== "EEXIST") {
3036
3079
  throw error;
3037
3080
  }
3038
3081
  }
3039
3082
  }
3040
- return written;
3083
+ return written;
3084
+ }
3085
+ async function writeStarterTools(repoRoot, profile, force) {
3086
+ const candidates = await detectToolCandidates(repoRoot, profile);
3087
+ const names = [];
3088
+ for (const candidate of candidates) {
3089
+ try {
3090
+ await createTool(
3091
+ repoRoot,
3092
+ {
3093
+ name: candidate.name,
3094
+ description: candidate.description,
3095
+ run: candidate.run,
3096
+ risk: candidate.risk,
3097
+ confirm: candidate.confirm
3098
+ },
3099
+ { actor: "human", force }
3100
+ );
3101
+ names.push(candidate.name);
3102
+ } catch (error) {
3103
+ if (error instanceof ToolCreateError) {
3104
+ continue;
3105
+ }
3106
+ throw error;
3107
+ }
3108
+ }
3109
+ return names;
3110
+ }
3111
+ async function initHarness(repoRoot, options = {}) {
3112
+ if (!options.force && await pathExists(projectManifestPath(repoRoot))) {
3113
+ throw new InitError(
3114
+ `A harness already exists at ${path21.join(".threadroot", "harness.yaml")}. Re-run with --force to overwrite.`
3115
+ );
3116
+ }
3117
+ const profile = await detectProfile(repoRoot, options.profile);
3118
+ const name = await detectName(repoRoot);
3119
+ const adapters = options.adapters ?? DEFAULT_ADAPTERS;
3120
+ const tools2 = await writeStarterTools(repoRoot, profile, options.force ?? false);
3121
+ const skills = await writeBuiltinSkills(repoRoot);
3122
+ const memory = await writeProjectMemory(repoRoot);
3123
+ const manifest = harnessManifestSchema.parse({
3124
+ name,
3125
+ version: 1,
3126
+ profile,
3127
+ adapters,
3128
+ tools: { allow: tools2 }
3129
+ });
3130
+ await writeManifest(repoRoot, manifest);
3131
+ let report;
3132
+ let rules = [];
3133
+ if (options.import !== false) {
3134
+ report = await importVendorFiles(repoRoot, { include: options.importFiles });
3135
+ if (report.canonicalBody.trim()) {
3136
+ await writeFile8(path21.join(repoRoot, AGENTS_FILE2), `${report.canonicalBody.trim()}
3137
+ `, "utf8");
3138
+ }
3139
+ rules = await writeImportedRules(repoRoot, report);
3140
+ }
3141
+ const { written } = await runCompile(repoRoot, { home: options.home });
3142
+ const exposed = options.expose ? (await exposeProject(repoRoot, { agents: options.expose })).entries.filter((entry) => entry.status !== "missing" && entry.status !== "skipped").map((entry) => entry.path) : [];
3143
+ return { name, profile, adapters, skills, tools: tools2, memory, rules, import: report, compiled: written, exposed };
3144
+ }
3145
+
3146
+ // src/core/status.ts
3147
+ async function harnessStatus(repoRoot, options = {}) {
3148
+ let harness;
3149
+ try {
3150
+ harness = await resolveHarness(repoRoot, { home: options.home });
3151
+ } catch (error) {
3152
+ if (error instanceof HarnessError) {
3153
+ return { exists: false };
3154
+ }
3155
+ throw error;
3156
+ }
3157
+ const files = await compile(repoRoot, harness);
3158
+ const drift = await detectDrift(repoRoot, files);
3159
+ return {
3160
+ exists: true,
3161
+ manifest: {
3162
+ name: harness.manifest.name,
3163
+ profile: harness.manifest.profile,
3164
+ adapters: harness.manifest.adapters,
3165
+ toolsAllow: harness.manifest.tools.allow
3166
+ },
3167
+ counts: {
3168
+ skills: harness.skills.length,
3169
+ rules: harness.rules.length,
3170
+ tools: harness.tools.length,
3171
+ memory: harness.memory.length
3172
+ },
3173
+ drift
3174
+ };
3041
3175
  }
3042
3176
 
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) {
3177
+ // src/core/bootstrap.ts
3178
+ var DEFAULT_TASK = "start this project";
3179
+ async function harnessExists(repoRoot) {
3180
+ return pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3181
+ }
3182
+ async function pathExists2(target) {
3050
3183
  try {
3051
- return await readFile9(filePath, "utf8");
3184
+ await stat9(target);
3185
+ return true;
3052
3186
  } catch (error) {
3053
3187
  if (error.code === "ENOENT") {
3054
- return void 0;
3188
+ return false;
3055
3189
  }
3056
3190
  throw error;
3057
3191
  }
3058
3192
  }
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];
3193
+ function modeFor(options) {
3194
+ return options.yes && !options.dryRun ? "write" : "dry-run";
3195
+ }
3196
+ async function bootstrapProject(repoRoot, options = {}) {
3197
+ const task = options.task?.trim() || DEFAULT_TASK;
3198
+ const mode = modeFor(options);
3199
+ const write2 = mode === "write";
3200
+ const notes = [];
3201
+ const existed = await harnessExists(repoRoot);
3202
+ let setup;
3203
+ let init;
3204
+ let exposed;
3205
+ if (!options.noGlobal) {
3206
+ setup = await setupGlobal({
3207
+ agents: options.agents ?? "all",
3208
+ mode,
3209
+ home: options.home,
3210
+ mcp: options.mcp
3211
+ });
3212
+ } else {
3213
+ notes.push("Skipped global setup because --no-global was set.");
3214
+ }
3215
+ if (!existed && !options.noInit) {
3216
+ if (write2) {
3217
+ init = await initHarness(repoRoot, {
3218
+ import: options.import,
3219
+ profile: options.profile,
3220
+ home: options.home
3221
+ });
3077
3222
  } else {
3078
- buffer.push(line);
3223
+ notes.push(`Would initialize local-only harness at ${path22.join(".threadroot", "harness.yaml")}.`);
3079
3224
  }
3225
+ } else if (existed) {
3226
+ notes.push("Existing harness detected; bootstrap will not reinitialize it.");
3227
+ } else {
3228
+ notes.push("Skipped project initialization because --no-init was set.");
3080
3229
  }
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;
3230
+ const hasHarnessAfterInit = existed || Boolean(init) || await pathExists2(path22.join(repoRoot, ".threadroot", "harness.yaml"));
3231
+ if (options.expose) {
3232
+ exposed = await exposeProject(repoRoot, {
3233
+ agents: options.expose,
3234
+ mode
3235
+ });
3236
+ }
3237
+ let status;
3238
+ let doctorReport;
3239
+ let context;
3240
+ if (hasHarnessAfterInit) {
3241
+ status = await harnessStatus(repoRoot, { home: options.home });
3242
+ doctorReport = await doctor(repoRoot, { home: options.home });
3243
+ if (status.exists) {
3244
+ context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3090
3245
  }
3091
- return !haystack.includes(normalize(section.text));
3092
- });
3246
+ } else {
3247
+ notes.push("Skipped doctor/status/context because no harness exists yet.");
3248
+ }
3249
+ if (!write2) {
3250
+ notes.push("Run `threadroot bootstrap --yes` to apply this plan.");
3251
+ }
3252
+ return {
3253
+ mode: write2 ? "write" : "plan",
3254
+ task,
3255
+ harnessExisted: existed,
3256
+ setup,
3257
+ init,
3258
+ expose: exposed,
3259
+ status,
3260
+ doctor: doctorReport,
3261
+ context,
3262
+ notes
3263
+ };
3093
3264
  }
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;
3265
+ async function startSession(repoRoot, options = {}) {
3266
+ const task = options.task?.trim() || DEFAULT_TASK;
3267
+ const status = await harnessStatus(repoRoot, { home: options.home });
3268
+ const notes = [];
3269
+ if (!status.exists) {
3270
+ return {
3271
+ task,
3272
+ status,
3273
+ notes: ["No harness found. Run `threadroot bootstrap --yes` first."]
3274
+ };
3104
3275
  }
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
- );
3276
+ const doctorReport = await doctor(repoRoot, { home: options.home });
3277
+ const context = await assembleContext(repoRoot, task, { home: options.home, fallbackSkills: true });
3278
+ return { task, status, doctor: doctorReport, context, notes };
3112
3279
  }
3113
- function globsToApplyTo(value) {
3114
- if (typeof value === "string") {
3115
- const first = value.split(",")[0]?.trim();
3116
- return first || void 0;
3280
+
3281
+ // src/commands/session-output.ts
3282
+ function printDoctor(report) {
3283
+ if (!report) {
3284
+ return;
3117
3285
  }
3118
- if (Array.isArray(value) && value.length > 0 && typeof value[0] === "string") {
3119
- return value[0].trim() || void 0;
3286
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3287
+ console.log(actionable.length === 0 ? "doctor: clean" : `doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`);
3288
+ for (const finding3 of report.findings.slice(0, 8)) {
3289
+ const label = finding3.severity === "info" ? "hint" : finding3.severity;
3290
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3291
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3292
+ }
3293
+ if (report.findings.length > 8) {
3294
+ console.log(`- ... ${report.findings.length - 8} more finding(s)`);
3120
3295
  }
3121
- return void 0;
3122
3296
  }
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";
3297
+ function printStatus(status) {
3298
+ if (!status) {
3299
+ return;
3300
+ }
3301
+ if (!status.exists) {
3302
+ console.log("harness: missing");
3303
+ return;
3304
+ }
3305
+ console.log(`harness: ${status.manifest.name} (${status.manifest.profile})`);
3306
+ console.log(`adapters: ${status.manifest.adapters.length > 0 ? status.manifest.adapters.join(", ") : "none (local-only)"}`);
3307
+ console.log(
3308
+ `objects: ${status.counts.skills} skills, ${status.counts.rules} rules, ${status.counts.tools} tools, ${status.counts.memory} memory`
3309
+ );
3126
3310
  }
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;
3311
+ function printContext(context) {
3312
+ if (!context) {
3313
+ return;
3314
+ }
3315
+ console.log(`task: ${context.task}`);
3316
+ if (context.skills.length > 0) {
3317
+ const skillLabel = context.skills.some((skill) => skill.score > 0) ? "relevant skills:" : "starter skills:";
3318
+ console.log(skillLabel);
3319
+ for (const skill of context.skills.slice(0, 8)) {
3320
+ console.log(`- ${skill.name} - ${skill.when}`);
3134
3321
  }
3135
- const content = await readIfExists3(path21.join(repoRoot, file));
3136
- if (content && content.trim()) {
3137
- prose.push({ file, content });
3322
+ } else {
3323
+ console.log("relevant skills: none matched; run `threadroot skills list` to inspect all skills.");
3324
+ }
3325
+ if (context.tools.length > 0) {
3326
+ console.log("available tools:");
3327
+ for (const tool of context.tools.slice(0, 8)) {
3328
+ console.log(`- ${tool.name} (${tool.risk}) - ${tool.description}`);
3138
3329
  }
3139
3330
  }
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();
3331
+ if (context.memory.length > 0) {
3332
+ console.log(`memory: ${context.memory.map((entry) => entry.type).join(", ")}`);
3151
3333
  }
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;
3334
+ }
3335
+ function printCommandMap() {
3336
+ console.log("agent command map:");
3337
+ console.log('- `threadroot start "<task>"` - begin a focused agent session');
3338
+ console.log('- `threadroot context "<task>"` - get relevant skills, tools, rules, and memory');
3339
+ console.log("- `threadroot doctor` - check harness health and trust issues");
3340
+ console.log("- `threadroot skills list|inspect|validate` - inspect skill capabilities");
3341
+ console.log("- `threadroot tools list|check` and `threadroot run <tool>` - use explicit local tools");
3342
+ console.log('- `threadroot remember "<note>"` - save durable handoff/project memory');
3343
+ }
3344
+ function printBootstrapReport(report) {
3345
+ console.log(`Threadroot bootstrap: ${report.mode === "write" ? "complete" : "plan"}`);
3346
+ if (report.setup) {
3347
+ console.log("global setup:");
3348
+ for (const entry of report.setup.entries) {
3349
+ const suffix = entry.message ? ` - ${entry.message}` : "";
3350
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3161
3351
  }
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
3352
  }
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
- };
3353
+ if (report.init) {
3354
+ console.log("project init: created local-only .threadroot/");
3355
+ } else if (report.harnessExisted) {
3356
+ console.log("project init: existing harness preserved");
3357
+ }
3358
+ if (report.expose) {
3359
+ console.log("project exposure:");
3360
+ for (const entry of report.expose.entries) {
3361
+ const suffix = entry.message ? ` - ${entry.message}` : "";
3362
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3363
+ }
3364
+ }
3365
+ printStatus(report.status);
3366
+ printDoctor(report.doctor);
3367
+ printContext(report.context);
3368
+ printCommandMap();
3369
+ for (const note of report.notes) {
3370
+ console.log(`note: ${note}`);
3371
+ }
3372
+ if (report.mode === "write") {
3373
+ console.log('Success: Threadroot is ready. Run `threadroot start "<task>"` for future sessions.');
3374
+ }
3375
+ }
3376
+ function printStartReport(report) {
3377
+ console.log("Threadroot start:");
3378
+ printStatus(report.status);
3379
+ printDoctor(report.doctor);
3380
+ printContext(report.context);
3381
+ printCommandMap();
3382
+ for (const note of report.notes) {
3383
+ console.log(`note: ${note}`);
3384
+ }
3385
+ }
3386
+
3387
+ // src/commands/bootstrap.ts
3388
+ async function runBootstrap(repoRoot, options) {
3389
+ const report = await bootstrapProject(repoRoot, {
3390
+ yes: options.yes,
3391
+ dryRun: options.dryRun,
3392
+ agents: options.agent,
3393
+ task: options.task,
3394
+ mcp: options.mcp,
3395
+ expose: options.expose,
3396
+ noGlobal: options.global === false,
3397
+ noInit: options.init === false,
3398
+ import: options.import,
3399
+ profile: options.profile ? profileIdSchema.parse(options.profile) : void 0
3175
3400
  });
3176
- return { canonicalSource, canonicalBody: body, foldedFrom, importedRules, skippedDuplicates };
3401
+ printBootstrapReport(report);
3402
+ if (report.mode === "write" && report.doctor && !report.doctor.ok) {
3403
+ process.exitCode = 1;
3404
+ }
3177
3405
  }
3178
3406
 
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";
3407
+ // src/commands/compile.ts
3408
+ async function runCompileCommand(repoRoot, options) {
3409
+ const adapter = options.adapter ? adapterIdSchema.parse(options.adapter) : void 0;
3410
+ try {
3411
+ const { written, drift } = await runCompile(repoRoot, { adapter });
3412
+ const changed = drift.filter((entry) => entry.status !== "unchanged").length;
3413
+ console.log(`Compiled ${written.length} vendor file(s)${changed > 0 ? ` (${changed} changed)` : ""}.`);
3414
+ for (const file of written) {
3415
+ console.log(` ${file}`);
3416
+ }
3417
+ } catch (error) {
3418
+ if (error instanceof HarnessError) {
3419
+ console.error(error.message);
3420
+ process.exitCode = 1;
3421
+ return;
3422
+ }
3423
+ throw error;
3186
3424
  }
3187
- };
3188
- async function pathExists(target) {
3425
+ }
3426
+
3427
+ // src/commands/context.ts
3428
+ async function runContext(repoRoot, task) {
3429
+ let context;
3189
3430
  try {
3190
- await stat8(target);
3191
- return true;
3192
- } catch {
3193
- return false;
3431
+ context = await assembleContext(repoRoot, task);
3432
+ } catch (error) {
3433
+ if (error instanceof HarnessError) {
3434
+ console.log("No harness found. Run `tr init` first.");
3435
+ return;
3436
+ }
3437
+ throw error;
3438
+ }
3439
+ console.log(`task: ${context.task}`);
3440
+ if (context.skills.length > 0) {
3441
+ console.log("\nskills:");
3442
+ for (const skill of context.skills) {
3443
+ console.log(`- ${skill.name} - ${skill.when}`);
3444
+ }
3445
+ }
3446
+ if (context.rules.length > 0) {
3447
+ console.log("\nrules:");
3448
+ for (const rule of context.rules) {
3449
+ console.log(`- ${rule.name}${rule.applyTo ? ` (${rule.applyTo})` : ""}`);
3450
+ }
3451
+ }
3452
+ if (context.tools.length > 0) {
3453
+ console.log("\ntools:");
3454
+ for (const tool of context.tools) {
3455
+ console.log(`- ${tool.name} - ${tool.description}`);
3456
+ }
3457
+ }
3458
+ if (context.memory.length > 0) {
3459
+ console.log("\nmemory:");
3460
+ for (const entry of context.memory) {
3461
+ console.log(`- ${entry.type}`);
3462
+ }
3463
+ }
3464
+ if (context.skills.length === 0 && context.rules.length === 0 && context.tools.length === 0 && context.memory.length === 0) {
3465
+ console.log("\nNo matching harness context for this task yet.");
3194
3466
  }
3195
3467
  }
3196
- async function detectProfile(repoRoot, override) {
3197
- if (override) {
3198
- return override;
3468
+
3469
+ // src/commands/diff.ts
3470
+ import fs3 from "fs/promises";
3471
+ import path23 from "path";
3472
+ async function readIfExists3(filePath) {
3473
+ try {
3474
+ return await fs3.readFile(filePath, "utf8");
3475
+ } catch (error) {
3476
+ if (error.code === "ENOENT") {
3477
+ return void 0;
3478
+ }
3479
+ throw error;
3199
3480
  }
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
3481
  }
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();
3482
+ function lineDiff(before, after) {
3483
+ const a = before.length === 0 ? [] : before.split("\n");
3484
+ const b = after.length === 0 ? [] : after.split("\n");
3485
+ const lcs = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
3486
+ for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
3487
+ for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
3488
+ lcs[i2][j2] = a[i2] === b[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
3489
+ }
3209
3490
  }
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 };
3491
+ const lines = [];
3492
+ let i = 0;
3493
+ let j = 0;
3494
+ while (i < a.length && j < b.length) {
3495
+ if (a[i] === b[j]) {
3496
+ i += 1;
3497
+ j += 1;
3498
+ } else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
3499
+ lines.push(`- ${a[i]}`);
3500
+ i += 1;
3501
+ } else {
3502
+ lines.push(`+ ${b[j]}`);
3503
+ j += 1;
3504
+ }
3221
3505
  }
3222
- await mkdir9(projectHarnessDir(repoRoot), { recursive: true });
3223
- await writeFile8(projectManifestPath(repoRoot), stringifyYaml4(body), "utf8");
3506
+ while (i < a.length) {
3507
+ lines.push(`- ${a[i]}`);
3508
+ i += 1;
3509
+ }
3510
+ while (j < b.length) {
3511
+ lines.push(`+ ${b[j]}`);
3512
+ j += 1;
3513
+ }
3514
+ return lines;
3224
3515
  }
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");
3516
+ async function runDiff(repoRoot) {
3517
+ let harness;
3229
3518
  try {
3230
- await writeFile8(filePath, `${PROJECT_MEMORY_TEMPLATE}
3231
- `, { encoding: "utf8", flag: "wx" });
3232
- return [filePath];
3519
+ harness = await resolveHarness(repoRoot);
3233
3520
  } catch (error) {
3234
- if (error.code === "EEXIST") {
3235
- return [];
3521
+ if (error instanceof HarnessError) {
3522
+ console.log("No harness found. Run `tr init` first.");
3523
+ return;
3236
3524
  }
3237
3525
  throw error;
3238
3526
  }
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;
3527
+ const files = await compile(repoRoot, harness);
3528
+ let changed = 0;
3529
+ for (const file of files) {
3530
+ const existing = await readIfExists3(path23.join(repoRoot, file.path));
3531
+ if (existing === void 0) {
3532
+ changed += 1;
3533
+ console.log(`+ ${file.path} (new)`);
3534
+ continue;
3252
3535
  }
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
- }
3536
+ if (existing === file.content) {
3537
+ continue;
3538
+ }
3539
+ changed += 1;
3540
+ console.log(`~ ${file.path}`);
3541
+ for (const line of lineDiff(existing, file.content)) {
3542
+ console.log(` ${line}`);
3260
3543
  }
3261
3544
  }
3262
- return written;
3545
+ if (changed === 0) {
3546
+ console.log("No drift: optional compiled outputs match the canonical harness.");
3547
+ }
3263
3548
  }
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;
3549
+
3550
+ // src/commands/doctor.ts
3551
+ async function runDoctor(repoRoot) {
3552
+ const report = await doctor(repoRoot);
3553
+ const actionable = report.findings.filter((finding3) => finding3.severity !== "info");
3554
+ const hints = report.findings.filter((finding3) => finding3.severity === "info");
3555
+ if (actionable.length === 0) {
3556
+ console.log("Threadroot doctor: clean");
3557
+ for (const finding3 of hints) {
3558
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3559
+ console.log(`- hint ${finding3.code}: ${finding3.message}${suffix}`);
3286
3560
  }
3561
+ return;
3287
3562
  }
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
- );
3563
+ console.log(
3564
+ `Threadroot doctor: ${report.summary.errors} error(s), ${report.summary.warnings} warning(s)`
3565
+ );
3566
+ for (const finding3 of report.findings) {
3567
+ const label = finding3.severity === "error" ? "error" : finding3.severity === "warning" ? "warning" : "hint";
3568
+ const suffix = finding3.path ? ` (${finding3.path})` : "";
3569
+ console.log(`- ${label} ${finding3.code}: ${finding3.message}${suffix}`);
3295
3570
  }
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 }
3571
+ if (!report.ok) {
3572
+ process.exitCode = 1;
3573
+ }
3574
+ }
3575
+
3576
+ // src/commands/expose.ts
3577
+ function modeFromOptions(options) {
3578
+ if (options.undo) return "undo";
3579
+ if (options.check) return "check";
3580
+ if (options.dryRun) return "dry-run";
3581
+ return "write";
3582
+ }
3583
+ async function runExpose(repoRoot, agent, options) {
3584
+ const mode = modeFromOptions(options);
3585
+ const result = await exposeProject(repoRoot, {
3586
+ agents: agent,
3587
+ mode,
3588
+ force: options.force
3308
3589
  });
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);
3590
+ const verb = mode === "dry-run" ? "Project exposure plan" : mode === "check" ? "Project exposure check" : mode === "undo" ? "Removed project exposure" : "Exposed Threadroot project skills";
3591
+ console.log(`${verb}:`);
3592
+ for (const entry of result.entries) {
3593
+ const suffix = entry.message ? ` - ${entry.message}` : "";
3594
+ console.log(`- ${entry.label}: ${entry.status} ${entry.path}${suffix}`);
3319
3595
  }
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
3596
  }
3324
3597
 
3325
3598
  // src/commands/init.ts
@@ -3369,13 +3642,13 @@ async function runInit(repoRoot, options) {
3369
3642
  }
3370
3643
 
3371
3644
  // src/commands/install.ts
3372
- import path25 from "path";
3645
+ import path26 from "path";
3373
3646
 
3374
3647
  // src/core/install/fetch.ts
3375
3648
  import { execFile } from "child_process";
3376
3649
  import { mkdtemp, rm as rm3 } from "fs/promises";
3377
3650
  import os2 from "os";
3378
- import path23 from "path";
3651
+ import path24 from "path";
3379
3652
  import { promisify } from "util";
3380
3653
  var run = promisify(execFile);
3381
3654
  function cloneUrl(ref) {
@@ -3396,7 +3669,7 @@ async function git(cwd, args) {
3396
3669
  }
3397
3670
  async function fetchGitSource(ref) {
3398
3671
  const url = cloneUrl(ref);
3399
- const dir = await mkdtemp(path23.join(os2.tmpdir(), "threadroot-fetch-"));
3672
+ const dir = await mkdtemp(path24.join(os2.tmpdir(), "threadroot-fetch-"));
3400
3673
  const cleanup = () => rm3(dir, { recursive: true, force: true });
3401
3674
  try {
3402
3675
  if (ref.ref) {
@@ -3418,9 +3691,9 @@ async function fetchGitSource(ref) {
3418
3691
  }
3419
3692
 
3420
3693
  // 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";
3694
+ import { cp as cp2, mkdir as mkdir10, readFile as readFile10, readdir as readdir5, stat as stat10, writeFile as writeFile9 } from "fs/promises";
3422
3695
  import { createHash as createHash2 } from "crypto";
3423
- import path24 from "path";
3696
+ import path25 from "path";
3424
3697
  var NAME_RE5 = /^[a-z0-9][a-z0-9-]*$/;
3425
3698
  var KIND_DIR = {
3426
3699
  skill: "skills",
@@ -3432,8 +3705,8 @@ function objectExt(kind) {
3432
3705
  return kind === "tool" || kind === "connection" ? ".yaml" : ".md";
3433
3706
  }
3434
3707
  function safeRepoPath(objectPath) {
3435
- const normalized = path24.normalize(objectPath);
3436
- if (path24.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path24.sep}`)) {
3708
+ const normalized = path25.normalize(objectPath);
3709
+ if (path25.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path25.sep}`)) {
3437
3710
  throw new Error(`Unsafe object path: ${objectPath}`);
3438
3711
  }
3439
3712
  return normalized;
@@ -3447,7 +3720,7 @@ function inferKind(objectPath, override) {
3447
3720
  if (segments.includes("tools")) return "tool";
3448
3721
  if (segments.includes("connections")) return "connection";
3449
3722
  if (segments.includes("rules")) return "rule";
3450
- const ext = path24.extname(objectPath).toLowerCase();
3723
+ const ext = path25.extname(objectPath).toLowerCase();
3451
3724
  if (ext === ".yaml" || ext === ".yml") return "tool";
3452
3725
  if (ext === ".md") return "skill";
3453
3726
  throw new Error(
@@ -3455,7 +3728,7 @@ function inferKind(objectPath, override) {
3455
3728
  );
3456
3729
  }
3457
3730
  function deriveName(objectPath) {
3458
- const base = path24.basename(objectPath, path24.extname(objectPath));
3731
+ const base = path25.basename(objectPath, path25.extname(objectPath));
3459
3732
  if (!NAME_RE5.test(base)) {
3460
3733
  throw new Error(`Invalid object name \`${base}\` (use lowercase letters, digits, and dashes).`);
3461
3734
  }
@@ -3467,8 +3740,8 @@ async function hashDirectory(root) {
3467
3740
  async function walk(dir) {
3468
3741
  const entries = (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
3469
3742
  for (const entry of entries) {
3470
- const full = path24.join(dir, entry.name);
3471
- const rel = path24.relative(root, full).split(path24.sep).join("/");
3743
+ const full = path25.join(dir, entry.name);
3744
+ const rel = path25.relative(root, full).split(path25.sep).join("/");
3472
3745
  if (entry.isSymbolicLink()) {
3473
3746
  throw new Error(`Refusing to install skill directory with symlink: ${rel}`);
3474
3747
  }
@@ -3488,7 +3761,7 @@ async function hashDirectory(root) {
3488
3761
  return hash.digest("hex");
3489
3762
  }
3490
3763
  async function validateSkillDirectory2(sourcePath, expectedName) {
3491
- const skillPath = path24.join(sourcePath, "SKILL.md");
3764
+ const skillPath = path25.join(sourcePath, "SKILL.md");
3492
3765
  const parsed = parseFrontmatter(await readFile10(skillPath, "utf8"));
3493
3766
  const result = skillFrontmatterSchema.safeParse(parsed.data);
3494
3767
  if (!result.success) {
@@ -3518,7 +3791,7 @@ async function installObject(repoRoot, rawSource, options = {}) {
3518
3791
  objectPath = safeRepoPath(within);
3519
3792
  refLabel = ref.ref;
3520
3793
  const fetched = await fetchGitSource(ref);
3521
- sourcePath = path24.join(fetched.dir, objectPath);
3794
+ sourcePath = path25.join(fetched.dir, objectPath);
3522
3795
  resolved = fetched.sha;
3523
3796
  cleanup = fetched.cleanup;
3524
3797
  } else {
@@ -3533,18 +3806,18 @@ async function installObject(repoRoot, rawSource, options = {}) {
3533
3806
  const destDir = scope === "user" ? userObjectDir(dirKey, options.home) : projectObjectDir(repoRoot, dirKey);
3534
3807
  let destPath;
3535
3808
  let integrity;
3536
- const info = await stat9(sourcePath);
3809
+ const info = await stat10(sourcePath);
3537
3810
  if (info.isDirectory()) {
3538
3811
  if (kind !== "skill") {
3539
3812
  throw new Error("Only skill objects may be installed from a directory.");
3540
3813
  }
3541
3814
  integrity = `sha256:${await validateSkillDirectory2(sourcePath, name)}`;
3542
- destPath = path24.join(destDir, name);
3815
+ destPath = path25.join(destDir, name);
3543
3816
  await mkdir10(destDir, { recursive: true });
3544
3817
  await cp2(sourcePath, destPath, { recursive: true, force: true });
3545
3818
  } else {
3546
3819
  const content = await readFile10(sourcePath, "utf8");
3547
- destPath = path24.join(destDir, `${name}${objectExt(kind)}`);
3820
+ destPath = path25.join(destDir, `${name}${objectExt(kind)}`);
3548
3821
  await mkdir10(destDir, { recursive: true });
3549
3822
  await writeFile9(destPath, content, "utf8");
3550
3823
  integrity = `sha256:${hashContent(content)}`;
@@ -3598,7 +3871,7 @@ async function runInstall(repoRoot, source, options) {
3598
3871
  if (installed.kind === "skill" && installed.entry.sourceKind !== "local") {
3599
3872
  console.log(" note: inspect external skills before trusting bundled scripts, assets, or allowed tools.");
3600
3873
  if (scope === "project") {
3601
- console.log(` inspect: threadroot skills inspect ${path25.relative(repoRoot, installed.path)}`);
3874
+ console.log(` inspect: threadroot skills inspect ${path26.relative(repoRoot, installed.path)}`);
3602
3875
  }
3603
3876
  }
3604
3877
  } catch (error) {
@@ -3611,39 +3884,6 @@ async function runInstall(repoRoot, source, options) {
3611
3884
  import readline from "readline";
3612
3885
  import { stdin as input, stdout as output } from "process";
3613
3886
  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
3887
  function defineTool(spec) {
3648
3888
  return spec;
3649
3889
  }
@@ -3922,7 +4162,7 @@ async function handleMessage(repoRoot, request) {
3922
4162
  if (request.method === "initialize") {
3923
4163
  return resultResponse(request, {
3924
4164
  protocolVersion: "2024-11-05",
3925
- serverInfo: { name: "threadroot", version: "0.1.1" },
4165
+ serverInfo: { name: "threadroot", version: "0.1.2" },
3926
4166
  capabilities: { tools: {} }
3927
4167
  });
3928
4168
  }
@@ -4064,29 +4304,28 @@ Repository:
4064
4304
  ${repoRoot}
4065
4305
 
4066
4306
  Goal:
4067
- Initialize Threadroot so this repo has a portable AI-agent harness in one canonical \`.threadroot/\` directory.
4307
+ Make this repository ready for agent-assisted development with minimal project clutter.
4068
4308
 
4069
4309
  Rules:
4070
4310
  - Prefer deterministic CLI commands.
4071
- - Do not overwrite user-owned files without checking Threadroot status/diff.
4311
+ - Do not invent Threadroot commands.
4072
4312
  - Keep context small. Use Threadroot context output before reading broad project files.
4313
+ - Do not create provider-specific project files unless the user asks.
4073
4314
 
4074
4315
  Steps:
4075
4316
  1. Check whether Threadroot is available with \`threadroot --version\`.
4076
4317
  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.
4318
+ 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"\`.
4319
+ 4. Run \`threadroot start "current task"\` with the user's actual task.
4320
+ 5. If the user asks for provider-native project skill files, run \`threadroot expose <agent>\` or \`threadroot expose all\`.
4321
+ 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
4322
 
4084
4323
  Final response:
4085
4324
  Say exactly:
4086
- "Success: Threadroot is initialized. Run \`threadroot status\` or \`threadroot context "<task>"\` to use it."
4325
+ "Success: Threadroot is ready. Run \`threadroot start "<task>"\` for future sessions."
4087
4326
 
4088
4327
  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."`;
4328
+ "Success: Threadroot is ready. Run \`${localCommand} start "<task>"\` for future sessions."`;
4090
4329
  }
4091
4330
  function parseAgent(value) {
4092
4331
  if (value === "codex" || value === "copilot" || value === "cursor" || value === "claude" || value === "generic") {
@@ -4147,10 +4386,10 @@ function agentNotes(agent) {
4147
4386
 
4148
4387
  // src/core/mcp-config.ts
4149
4388
  import { mkdir as mkdir11, readFile as readFile11, writeFile as writeFile10 } from "fs/promises";
4150
- import path26 from "path";
4389
+ import path27 from "path";
4151
4390
  var TARGETS = [
4152
- { agent: "copilot", file: path26.join(".vscode", "mcp.json"), key: "servers" },
4153
- { agent: "cursor", file: path26.join(".cursor", "mcp.json"), key: "mcpServers" },
4391
+ { agent: "copilot", file: path27.join(".vscode", "mcp.json"), key: "servers" },
4392
+ { agent: "cursor", file: path27.join(".cursor", "mcp.json"), key: "mcpServers" },
4154
4393
  { agent: "claude", file: ".mcp.json", key: "mcpServers" }
4155
4394
  ];
4156
4395
  function mcpServerEntry(command, scriptPath) {
@@ -4172,7 +4411,7 @@ async function mergeConfig(filePath, key, entry) {
4172
4411
  const servers = config[key] && typeof config[key] === "object" ? config[key] : {};
4173
4412
  servers.threadroot = { ...entry };
4174
4413
  config[key] = servers;
4175
- await mkdir11(path26.dirname(filePath), { recursive: true });
4414
+ await mkdir11(path27.dirname(filePath), { recursive: true });
4176
4415
  await writeFile10(filePath, `${JSON.stringify(config, null, 2)}
4177
4416
  `, "utf8");
4178
4417
  }
@@ -4181,7 +4420,7 @@ async function writeProjectMcpConfigs(input2) {
4181
4420
  const targets = agents ? TARGETS.filter((target) => agents.includes(target.agent)) : TARGETS;
4182
4421
  const written = [];
4183
4422
  for (const target of targets) {
4184
- const filePath = path26.join(input2.repoRoot, target.file);
4423
+ const filePath = path27.join(input2.repoRoot, target.file);
4185
4424
  await mergeConfig(filePath, target.key, input2.entry);
4186
4425
  written.push(target.file);
4187
4426
  }
@@ -4314,6 +4553,15 @@ async function runSetup(_repoRoot, options) {
4314
4553
  }
4315
4554
  }
4316
4555
 
4556
+ // src/commands/start.ts
4557
+ async function runStart(repoRoot, task, options) {
4558
+ const report = await startSession(repoRoot, { task: task ?? options.task });
4559
+ printStartReport(report);
4560
+ if (!report.status.exists || report.doctor && !report.doctor.ok) {
4561
+ process.exitCode = 1;
4562
+ }
4563
+ }
4564
+
4317
4565
  // src/commands/status.ts
4318
4566
  async function runStatus(repoRoot) {
4319
4567
  const status = await harnessStatus(repoRoot);
@@ -4338,8 +4586,8 @@ async function runStatus(repoRoot) {
4338
4586
  }
4339
4587
 
4340
4588
  // 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";
4589
+ import { cp as cp3, mkdir as mkdir12, readFile as readFile12, readdir as readdir6, stat as stat11 } from "fs/promises";
4590
+ import path28 from "path";
4343
4591
  import { fileURLToPath as fileURLToPath2 } from "url";
4344
4592
  import { parse as parseYaml3 } from "yaml";
4345
4593
  import { z as z5 } from "zod";
@@ -4352,18 +4600,18 @@ var packManifestSchema = z5.object({
4352
4600
  rules: z5.array(z5.string()).default([]),
4353
4601
  connections: z5.array(z5.string()).default([])
4354
4602
  });
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, "../../../..");
4603
+ var DIST_DIR2 = path28.dirname(fileURLToPath2(import.meta.url));
4604
+ var PACKAGE_ROOT_FROM_BUNDLE2 = path28.resolve(DIST_DIR2, "..");
4605
+ var PACKAGE_ROOT_FROM_DIST2 = path28.resolve(DIST_DIR2, "../../..");
4606
+ var PACKAGE_ROOT_FROM_SRC2 = path28.resolve(DIST_DIR2, "../../../..");
4359
4607
  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")
4608
+ path28.join(PACKAGE_ROOT_FROM_BUNDLE2, "packs"),
4609
+ path28.join(PACKAGE_ROOT_FROM_DIST2, "packs"),
4610
+ path28.join(PACKAGE_ROOT_FROM_SRC2, "packs")
4363
4611
  ];
4364
4612
  async function exists5(target) {
4365
4613
  try {
4366
- await stat10(target);
4614
+ await stat11(target);
4367
4615
  return true;
4368
4616
  } catch (error) {
4369
4617
  if (error.code === "ENOENT") {
@@ -4391,21 +4639,21 @@ async function isPackRoot(candidate) {
4391
4639
  return false;
4392
4640
  }
4393
4641
  for (const entry of entries) {
4394
- if (entry.isDirectory() && await exists5(path27.join(candidate, entry.name, "pack.yaml"))) {
4642
+ if (entry.isDirectory() && await exists5(path28.join(candidate, entry.name, "pack.yaml"))) {
4395
4643
  return true;
4396
4644
  }
4397
4645
  }
4398
4646
  return false;
4399
4647
  }
4400
4648
  function safeRelative(ref) {
4401
- const normalized = path27.normalize(ref);
4402
- if (path27.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path27.sep}`)) {
4649
+ const normalized = path28.normalize(ref);
4650
+ if (path28.isAbsolute(normalized) || normalized === ".." || normalized.startsWith(`..${path28.sep}`)) {
4403
4651
  throw new Error(`Unsafe pack reference: ${ref}`);
4404
4652
  }
4405
4653
  return normalized;
4406
4654
  }
4407
4655
  async function readPackManifest(packDir) {
4408
- const file = path27.join(packDir, "pack.yaml");
4656
+ const file = path28.join(packDir, "pack.yaml");
4409
4657
  const parsed = packManifestSchema.safeParse(parseYaml3(await readFile12(file, "utf8")));
4410
4658
  if (!parsed.success) {
4411
4659
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
@@ -4414,7 +4662,7 @@ async function readPackManifest(packDir) {
4414
4662
  return parsed.data;
4415
4663
  }
4416
4664
  async function packDirFor(repoRoot, nameOrPath) {
4417
- if (path27.isAbsolute(nameOrPath)) {
4665
+ if (path28.isAbsolute(nameOrPath)) {
4418
4666
  return nameOrPath;
4419
4667
  }
4420
4668
  if (nameOrPath.startsWith(".") || nameOrPath.includes("/") || nameOrPath.includes("\\")) {
@@ -4422,14 +4670,14 @@ async function packDirFor(repoRoot, nameOrPath) {
4422
4670
  }
4423
4671
  const bundled = await bundledPacksDir();
4424
4672
  if (bundled) {
4425
- return path27.join(bundled, nameOrPath);
4673
+ return path28.join(bundled, nameOrPath);
4426
4674
  }
4427
- return toRepoPath(repoRoot, path27.join("packs", nameOrPath));
4675
+ return toRepoPath(repoRoot, path28.join("packs", nameOrPath));
4428
4676
  }
4429
4677
  async function directFiles(dir, ext) {
4430
4678
  try {
4431
4679
  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();
4680
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => path28.join(dir, entry.name)).sort();
4433
4681
  } catch (error) {
4434
4682
  if (error.code === "ENOENT") {
4435
4683
  return [];
@@ -4442,11 +4690,11 @@ async function skillEntries(dir) {
4442
4690
  const entries = await readdir6(dir, { withFileTypes: true });
4443
4691
  const result = [];
4444
4692
  for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
4445
- const full = path27.join(dir, entry.name);
4693
+ const full = path28.join(dir, entry.name);
4446
4694
  if (entry.isFile() && entry.name.endsWith(".md")) {
4447
4695
  result.push(full);
4448
4696
  }
4449
- if (entry.isDirectory() && await exists5(path27.join(full, "SKILL.md"))) {
4697
+ if (entry.isDirectory() && await exists5(path28.join(full, "SKILL.md"))) {
4450
4698
  result.push(full);
4451
4699
  }
4452
4700
  }
@@ -4461,34 +4709,34 @@ async function skillEntries(dir) {
4461
4709
  async function collectObjects(packDir, manifest) {
4462
4710
  async function resolveRef(ref) {
4463
4711
  const safe = safeRelative(ref);
4464
- const local = path27.resolve(packDir, safe);
4712
+ const local = path28.resolve(packDir, safe);
4465
4713
  if (await exists5(local)) {
4466
4714
  return local;
4467
4715
  }
4468
- return path27.resolve(packDir, "..", "..", safe);
4716
+ return path28.resolve(packDir, "..", "..", safe);
4469
4717
  }
4470
4718
  return {
4471
4719
  skills: [
4472
4720
  ...await Promise.all(manifest.skills.map(resolveRef)),
4473
- ...await skillEntries(path27.join(packDir, "skills"))
4721
+ ...await skillEntries(path28.join(packDir, "skills"))
4474
4722
  ],
4475
4723
  tools: [
4476
4724
  ...await Promise.all(manifest.tools.map(resolveRef)),
4477
- ...await directFiles(path27.join(packDir, "tools"), ".yaml")
4725
+ ...await directFiles(path28.join(packDir, "tools"), ".yaml")
4478
4726
  ],
4479
4727
  rules: [
4480
4728
  ...await Promise.all(manifest.rules.map(resolveRef)),
4481
- ...await directFiles(path27.join(packDir, "rules"), ".md")
4729
+ ...await directFiles(path28.join(packDir, "rules"), ".md")
4482
4730
  ],
4483
4731
  connections: [
4484
4732
  ...await Promise.all(manifest.connections.map(resolveRef)),
4485
- ...await directFiles(path27.join(packDir, "connections"), ".yaml")
4733
+ ...await directFiles(path28.join(packDir, "connections"), ".yaml")
4486
4734
  ]
4487
4735
  };
4488
4736
  }
4489
4737
  function baseName(source) {
4490
- const parsed = path27.basename(source) === "SKILL.md" ? path27.dirname(source) : source;
4491
- return path27.basename(parsed, path27.extname(parsed));
4738
+ const parsed = path28.basename(source) === "SKILL.md" ? path28.dirname(source) : source;
4739
+ return path28.basename(parsed, path28.extname(parsed));
4492
4740
  }
4493
4741
  async function listPacks(repoRoot) {
4494
4742
  const dirs = [toRepoPath(repoRoot, "packs"), await bundledPacksDir()].filter((dir) => Boolean(dir));
@@ -4505,8 +4753,8 @@ async function listPacks(repoRoot) {
4505
4753
  if (!entry.isDirectory() || seen.has(entry.name)) {
4506
4754
  continue;
4507
4755
  }
4508
- const packDir = path27.join(root, entry.name);
4509
- if (!await exists5(path27.join(packDir, "pack.yaml"))) {
4756
+ const packDir = path28.join(root, entry.name);
4757
+ if (!await exists5(path28.join(packDir, "pack.yaml"))) {
4510
4758
  continue;
4511
4759
  }
4512
4760
  seen.add(entry.name);
@@ -4530,7 +4778,7 @@ async function inspectPack(repoRoot, nameOrPath) {
4530
4778
  };
4531
4779
  }
4532
4780
  async function validateProse(file, kind) {
4533
- const target = path27.basename(file) === "SKILL.md" ? file : file;
4781
+ const target = path28.basename(file) === "SKILL.md" ? file : file;
4534
4782
  const content = await readFile12(target, "utf8");
4535
4783
  const parsed = parseFrontmatter(content);
4536
4784
  const schema = kind === "skill" ? skillFrontmatterSchema : ruleFrontmatterSchema;
@@ -4548,7 +4796,7 @@ async function validatePack(repoRoot, nameOrPath) {
4548
4796
  const manifest = await readPackManifest(packDir);
4549
4797
  const objects = await collectObjects(packDir, manifest);
4550
4798
  for (const skill of objects.skills) {
4551
- await validateProse(path27.basename(skill) === "SKILL.md" ? skill : path27.join(skill, "SKILL.md"), "skill");
4799
+ await validateProse(path28.basename(skill) === "SKILL.md" ? skill : path28.join(skill, "SKILL.md"), "skill");
4552
4800
  }
4553
4801
  for (const rule of objects.rules) await validateProse(rule, "rule");
4554
4802
  for (const tool of objects.tools) await validateYaml(tool, "tool");
@@ -4562,9 +4810,9 @@ async function validatePack(repoRoot, nameOrPath) {
4562
4810
  return { ok: !findings.some((finding3) => finding3.severity === "error"), findings };
4563
4811
  }
4564
4812
  async function copyObject(source, destDir) {
4565
- const info = await stat10(source);
4813
+ const info = await stat11(source);
4566
4814
  const name = baseName(source);
4567
- const dest = info.isDirectory() ? path27.join(destDir, name) : path27.join(destDir, path27.basename(source));
4815
+ const dest = info.isDirectory() ? path28.join(destDir, name) : path28.join(destDir, path28.basename(source));
4568
4816
  await mkdir12(destDir, { recursive: true });
4569
4817
  await cp3(source, dest, { recursive: true, force: true });
4570
4818
  return dest;
@@ -4853,7 +5101,9 @@ async function runConnectionsCheck(repoRoot) {
4853
5101
  // src/cli.ts
4854
5102
  function createProgram(repoRoot = process.cwd()) {
4855
5103
  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");
5104
+ program.name("threadroot").description("Git for your AI agent harness: one command to bootstrap, one .threadroot source.").version("0.1.2");
5105
+ 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));
5106
+ 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
5107
  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
5108
  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
5109
  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));