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