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