first-tree 0.0.2 → 0.0.4
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/README.md +116 -40
- package/dist/cli.js +46 -17
- package/dist/help-Dtdj91HJ.js +25 -0
- package/dist/init--VepFe6N.js +403 -0
- package/dist/installer-cH7N4RNj.js +47 -0
- package/dist/onboarding-C9cYSE6F.js +2 -0
- package/dist/onboarding-CPP8fF4D.js +10 -0
- package/dist/repo-DY57bMqr.js +318 -0
- package/dist/upgrade-Cgx_K2HM.js +135 -0
- package/dist/{verify-CSRIkuoM.js → verify-mC9ZTd1f.js} +118 -29
- package/package.json +33 -10
- package/skills/first-tree/SKILL.md +113 -0
- package/skills/first-tree/agents/openai.yaml +4 -0
- package/skills/first-tree/assets/framework/VERSION +1 -0
- package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
- package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
- package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
- package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
- package/skills/first-tree/assets/framework/helpers/run-review.ts +193 -0
- package/skills/first-tree/assets/framework/manifest.json +11 -0
- package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
- package/skills/first-tree/assets/framework/templates/agents.md.template +49 -0
- package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
- package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +41 -0
- package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
- package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
- package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
- package/skills/first-tree/engine/commands/help.ts +32 -0
- package/skills/first-tree/engine/commands/init.ts +1 -0
- package/skills/first-tree/engine/commands/upgrade.ts +1 -0
- package/skills/first-tree/engine/commands/verify.ts +1 -0
- package/skills/first-tree/engine/init.ts +414 -0
- package/skills/first-tree/engine/onboarding.ts +10 -0
- package/skills/first-tree/engine/repo.ts +360 -0
- package/skills/first-tree/engine/rules/agent-instructions.ts +59 -0
- package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
- package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
- package/skills/first-tree/engine/rules/framework.ts +13 -0
- package/skills/first-tree/engine/rules/index.ts +41 -0
- package/skills/first-tree/engine/rules/members.ts +21 -0
- package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
- package/skills/first-tree/engine/rules/root-node.ts +41 -0
- package/skills/first-tree/engine/runtime/adapters.ts +22 -0
- package/skills/first-tree/engine/runtime/asset-loader.ts +141 -0
- package/skills/first-tree/engine/runtime/installer.ts +82 -0
- package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
- package/skills/first-tree/engine/upgrade.ts +233 -0
- package/skills/first-tree/engine/validators/members.ts +215 -0
- package/skills/first-tree/engine/validators/nodes.ts +559 -0
- package/skills/first-tree/engine/verify.ts +155 -0
- package/skills/first-tree/references/about.md +36 -0
- package/skills/first-tree/references/maintainer-architecture.md +59 -0
- package/skills/first-tree/references/maintainer-build-and-distribution.md +59 -0
- package/skills/first-tree/references/maintainer-testing.md +58 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
- package/skills/first-tree/references/onboarding.md +185 -0
- package/skills/first-tree/references/ownership-and-naming.md +94 -0
- package/skills/first-tree/references/principles.md +113 -0
- package/skills/first-tree/references/source-map.md +94 -0
- package/skills/first-tree/references/upgrade-contract.md +94 -0
- package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
- package/skills/first-tree/scripts/quick_validate.py +95 -0
- package/skills/first-tree/scripts/run-local-cli.sh +35 -0
- package/skills/first-tree/tests/asset-loader.test.ts +75 -0
- package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
- package/skills/first-tree/tests/helpers.ts +169 -0
- package/skills/first-tree/tests/init.test.ts +250 -0
- package/skills/first-tree/tests/repo.test.ts +440 -0
- package/skills/first-tree/tests/rules.test.ts +413 -0
- package/skills/first-tree/tests/run-review.test.ts +155 -0
- package/skills/first-tree/tests/skill-artifacts.test.ts +311 -0
- package/skills/first-tree/tests/thin-cli.test.ts +104 -0
- package/skills/first-tree/tests/upgrade.test.ts +103 -0
- package/skills/first-tree/tests/validate-members.test.ts +224 -0
- package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
- package/skills/first-tree/tests/verify.test.ts +241 -0
- package/dist/init-CE_944sb.js +0 -283
- package/dist/repo-BByc3VvM.js +0 -111
- package/dist/upgrade-Chr7z0CY.js +0 -82
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
5
|
+
FRAMEWORK_VERSION,
|
|
6
|
+
LEGACY_SKILL_PROGRESS,
|
|
7
|
+
LEGACY_SKILL_VERSION,
|
|
8
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
9
|
+
LEGACY_PROGRESS,
|
|
10
|
+
LEGACY_VERSION,
|
|
11
|
+
INSTALLED_PROGRESS,
|
|
12
|
+
agentInstructionsFileCandidates,
|
|
13
|
+
type FrameworkLayout,
|
|
14
|
+
detectFrameworkLayout,
|
|
15
|
+
frameworkVersionCandidates,
|
|
16
|
+
progressFileCandidates,
|
|
17
|
+
resolveFirstExistingPath,
|
|
18
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
19
|
+
|
|
20
|
+
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
21
|
+
const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
|
|
22
|
+
const TITLE_RE = /^title:\s*['"]?(.+?)['"]?\s*$/m;
|
|
23
|
+
const EMPTY_REPO_ENTRY_ALLOWLIST = new Set([
|
|
24
|
+
".DS_Store",
|
|
25
|
+
".editorconfig",
|
|
26
|
+
".gitattributes",
|
|
27
|
+
".github",
|
|
28
|
+
".gitignore",
|
|
29
|
+
"AGENT.md",
|
|
30
|
+
"AGENTS.md",
|
|
31
|
+
"CLAUDE.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"LICENSE.md",
|
|
34
|
+
"LICENSE.txt",
|
|
35
|
+
"README",
|
|
36
|
+
"README.md",
|
|
37
|
+
"README.txt",
|
|
38
|
+
]);
|
|
39
|
+
const SOURCE_FILE_HINTS = new Set([
|
|
40
|
+
".gitmodules",
|
|
41
|
+
"Cargo.toml",
|
|
42
|
+
"Dockerfile",
|
|
43
|
+
"Gemfile",
|
|
44
|
+
"Makefile",
|
|
45
|
+
"bun.lock",
|
|
46
|
+
"bun.lockb",
|
|
47
|
+
"docker-compose.yml",
|
|
48
|
+
"go.mod",
|
|
49
|
+
"package-lock.json",
|
|
50
|
+
"package.json",
|
|
51
|
+
"pnpm-lock.yaml",
|
|
52
|
+
"pyproject.toml",
|
|
53
|
+
"requirements.txt",
|
|
54
|
+
"tsconfig.json",
|
|
55
|
+
"uv.lock",
|
|
56
|
+
"vite.config.ts",
|
|
57
|
+
"vite.config.js",
|
|
58
|
+
]);
|
|
59
|
+
const SOURCE_DIR_HINTS = new Set([
|
|
60
|
+
"app",
|
|
61
|
+
"apps",
|
|
62
|
+
"backend",
|
|
63
|
+
"cli",
|
|
64
|
+
"client",
|
|
65
|
+
"docs",
|
|
66
|
+
"e2e",
|
|
67
|
+
"frontend",
|
|
68
|
+
"lib",
|
|
69
|
+
"packages",
|
|
70
|
+
"scripts",
|
|
71
|
+
"server",
|
|
72
|
+
"src",
|
|
73
|
+
"test",
|
|
74
|
+
"tests",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
export const FRAMEWORK_BEGIN_MARKER = "<!-- BEGIN CONTEXT-TREE FRAMEWORK";
|
|
78
|
+
export const FRAMEWORK_END_MARKER = "<!-- END CONTEXT-TREE FRAMEWORK -->";
|
|
79
|
+
|
|
80
|
+
export interface Frontmatter {
|
|
81
|
+
title?: string;
|
|
82
|
+
owners?: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hasGitMetadata(root: string): boolean {
|
|
86
|
+
try {
|
|
87
|
+
const stat = statSync(join(root, ".git"));
|
|
88
|
+
return stat.isDirectory() || stat.isFile();
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function discoverGitRoot(start: string): string | null {
|
|
95
|
+
let dir = start;
|
|
96
|
+
while (true) {
|
|
97
|
+
if (hasGitMetadata(dir)) {
|
|
98
|
+
return dir;
|
|
99
|
+
}
|
|
100
|
+
const parent = dirname(dir);
|
|
101
|
+
if (parent === dir) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
dir = parent;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export class Repo {
|
|
109
|
+
readonly root: string;
|
|
110
|
+
|
|
111
|
+
constructor(root?: string) {
|
|
112
|
+
const start = resolve(root ?? process.cwd());
|
|
113
|
+
this.root = root === undefined ? discoverGitRoot(start) ?? start : start;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pathExists(relPath: string): boolean {
|
|
117
|
+
return existsSync(join(this.root, relPath));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fileContains(relPath: string, text: string): boolean {
|
|
121
|
+
const fullPath = join(this.root, relPath);
|
|
122
|
+
try {
|
|
123
|
+
const stat = statSync(fullPath);
|
|
124
|
+
if (!stat.isFile()) return false;
|
|
125
|
+
return readFileSync(fullPath, "utf-8").includes(text);
|
|
126
|
+
} catch {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
readFile(relPath: string): string | null {
|
|
132
|
+
try {
|
|
133
|
+
return readFileSync(join(this.root, relPath), "utf-8");
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
frontmatter(relPath: string): Frontmatter | null {
|
|
140
|
+
const text = this.readFile(relPath);
|
|
141
|
+
if (text === null) return null;
|
|
142
|
+
const m = text.match(FRONTMATTER_RE);
|
|
143
|
+
if (!m) return null;
|
|
144
|
+
const fm = m[1];
|
|
145
|
+
const result: Frontmatter = {};
|
|
146
|
+
const titleM = fm.match(TITLE_RE);
|
|
147
|
+
if (titleM) {
|
|
148
|
+
result.title = titleM[1].trim();
|
|
149
|
+
}
|
|
150
|
+
const ownersM = fm.match(OWNERS_RE);
|
|
151
|
+
if (ownersM) {
|
|
152
|
+
const raw = ownersM[1].trim();
|
|
153
|
+
result.owners = raw
|
|
154
|
+
? raw.split(",").map((o) => o.trim()).filter(Boolean)
|
|
155
|
+
: [];
|
|
156
|
+
}
|
|
157
|
+
return result.title !== undefined || result.owners !== undefined
|
|
158
|
+
? result
|
|
159
|
+
: null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
anyAgentConfig(): boolean {
|
|
163
|
+
const knownConfigs = [".claude/settings.json", ".codex/config.json"];
|
|
164
|
+
return knownConfigs.some((c) => this.pathExists(c));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
isGitRepo(): boolean {
|
|
168
|
+
return hasGitMetadata(this.root);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
hasFramework(): boolean {
|
|
172
|
+
return this.frameworkLayout() !== null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
frameworkLayout(): FrameworkLayout | null {
|
|
176
|
+
return detectFrameworkLayout(this.root);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
readVersion(): string | null {
|
|
180
|
+
const versionPath = resolveFirstExistingPath(
|
|
181
|
+
this.root,
|
|
182
|
+
frameworkVersionCandidates(),
|
|
183
|
+
);
|
|
184
|
+
if (versionPath === null) return null;
|
|
185
|
+
const text = this.readFile(versionPath);
|
|
186
|
+
return text ? text.trim() : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
progressPath(): string | null {
|
|
190
|
+
return resolveFirstExistingPath(this.root, progressFileCandidates());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
preferredProgressPath(): string {
|
|
194
|
+
const layout = this.frameworkLayout();
|
|
195
|
+
if (layout === "legacy") {
|
|
196
|
+
return LEGACY_PROGRESS;
|
|
197
|
+
}
|
|
198
|
+
if (layout === "legacy-skill") {
|
|
199
|
+
return LEGACY_SKILL_PROGRESS;
|
|
200
|
+
}
|
|
201
|
+
return INSTALLED_PROGRESS;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
frameworkVersionPath(): string {
|
|
205
|
+
const layout = this.frameworkLayout();
|
|
206
|
+
if (layout === "legacy") {
|
|
207
|
+
return LEGACY_VERSION;
|
|
208
|
+
}
|
|
209
|
+
if (layout === "legacy-skill") {
|
|
210
|
+
return LEGACY_SKILL_VERSION;
|
|
211
|
+
}
|
|
212
|
+
return FRAMEWORK_VERSION;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
agentInstructionsPath(): string | null {
|
|
216
|
+
return resolveFirstExistingPath(this.root, agentInstructionsFileCandidates());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
hasCanonicalAgentInstructionsFile(): boolean {
|
|
220
|
+
return this.pathExists(AGENT_INSTRUCTIONS_FILE);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
hasLegacyAgentInstructionsFile(): boolean {
|
|
224
|
+
return this.pathExists(LEGACY_AGENT_INSTRUCTIONS_FILE);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
hasDuplicateAgentInstructionsFiles(): boolean {
|
|
228
|
+
return this.hasCanonicalAgentInstructionsFile() && this.hasLegacyAgentInstructionsFile();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
readAgentInstructions(): string | null {
|
|
232
|
+
const relPath = this.agentInstructionsPath();
|
|
233
|
+
if (relPath === null) return null;
|
|
234
|
+
return this.readFile(relPath);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
hasAgentInstructionsMarkers(): boolean {
|
|
238
|
+
const text = this.readAgentInstructions();
|
|
239
|
+
if (text === null) return false;
|
|
240
|
+
return text.includes(FRAMEWORK_BEGIN_MARKER) && text.includes(FRAMEWORK_END_MARKER);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
hasMembers(): boolean {
|
|
244
|
+
const membersDir = join(this.root, "members");
|
|
245
|
+
try {
|
|
246
|
+
if (!statSync(membersDir).isDirectory()) return false;
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return existsSync(join(membersDir, "NODE.md"));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
memberCount(): number {
|
|
254
|
+
const membersDir = join(this.root, "members");
|
|
255
|
+
try {
|
|
256
|
+
if (!statSync(membersDir).isDirectory()) return 0;
|
|
257
|
+
} catch {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
let count = 0;
|
|
261
|
+
const walk = (dir: string): void => {
|
|
262
|
+
for (const entry of readdirSync(dir)) {
|
|
263
|
+
const childPath = join(dir, entry);
|
|
264
|
+
try {
|
|
265
|
+
if (!statSync(childPath).isDirectory()) continue;
|
|
266
|
+
} catch {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (existsSync(join(childPath, "NODE.md"))) {
|
|
270
|
+
count++;
|
|
271
|
+
}
|
|
272
|
+
walk(childPath);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
walk(membersDir);
|
|
276
|
+
return count;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
hasPlaceholderNode(): boolean {
|
|
280
|
+
return this.fileContains("NODE.md", "<!-- PLACEHOLDER");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
repoName(): string {
|
|
284
|
+
return basename(this.root);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
topLevelEntries(): string[] {
|
|
288
|
+
try {
|
|
289
|
+
return readdirSync(this.root).filter((entry) => entry !== ".git");
|
|
290
|
+
} catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
looksLikeTreeRepo(): boolean {
|
|
296
|
+
if (
|
|
297
|
+
this.pathExists("package.json")
|
|
298
|
+
&& this.pathExists("src/cli.ts")
|
|
299
|
+
&& this.pathExists("skills/first-tree/SKILL.md")
|
|
300
|
+
&& this.progressPath() === null
|
|
301
|
+
&& this.frontmatter("NODE.md") === null
|
|
302
|
+
&& !this.hasAgentInstructionsMarkers()
|
|
303
|
+
&& !this.pathExists("members/NODE.md")
|
|
304
|
+
) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
this.progressPath() !== null
|
|
310
|
+
|| this.hasFramework()
|
|
311
|
+
|| this.hasAgentInstructionsMarkers()
|
|
312
|
+
|| this.pathExists("members/NODE.md")
|
|
313
|
+
|| this.frontmatter("NODE.md") !== null
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
isLikelyEmptyRepo(): boolean {
|
|
318
|
+
const relevant = this.topLevelEntries().filter(
|
|
319
|
+
(entry) => !EMPTY_REPO_ENTRY_ALLOWLIST.has(entry),
|
|
320
|
+
);
|
|
321
|
+
return relevant.length === 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
isLikelySourceRepo(): boolean {
|
|
325
|
+
if (this.looksLikeTreeRepo()) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const entries = this.topLevelEntries().filter(
|
|
330
|
+
(entry) => !EMPTY_REPO_ENTRY_ALLOWLIST.has(entry),
|
|
331
|
+
);
|
|
332
|
+
if (entries.length === 0) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let directoryCount = 0;
|
|
337
|
+
|
|
338
|
+
for (const entry of entries) {
|
|
339
|
+
if (SOURCE_FILE_HINTS.has(entry)) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
if (isDirectory(this.root, entry)) {
|
|
343
|
+
directoryCount += 1;
|
|
344
|
+
if (SOURCE_DIR_HINTS.has(entry)) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return directoryCount >= 2 || entries.length >= 4;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function isDirectory(root: string, relPath: string): boolean {
|
|
355
|
+
try {
|
|
356
|
+
return statSync(join(root, relPath)).isDirectory();
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FRAMEWORK_END_MARKER } from "#skill/engine/repo.js";
|
|
2
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
3
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
4
|
+
import {
|
|
5
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
6
|
+
AGENT_INSTRUCTIONS_TEMPLATE,
|
|
7
|
+
FRAMEWORK_TEMPLATES_DIR,
|
|
8
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
9
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
10
|
+
|
|
11
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
12
|
+
const tasks: string[] = [];
|
|
13
|
+
const hasCanonicalInstructions = repo.hasCanonicalAgentInstructionsFile();
|
|
14
|
+
const hasLegacyInstructions = repo.hasLegacyAgentInstructionsFile();
|
|
15
|
+
|
|
16
|
+
if (!hasCanonicalInstructions && !hasLegacyInstructions) {
|
|
17
|
+
tasks.push(
|
|
18
|
+
`${AGENT_INSTRUCTIONS_FILE} is missing — create from \`${FRAMEWORK_TEMPLATES_DIR}/${AGENT_INSTRUCTIONS_TEMPLATE}\``,
|
|
19
|
+
);
|
|
20
|
+
return { group: "Agent Instructions", order: 3, tasks };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (hasCanonicalInstructions && hasLegacyInstructions) {
|
|
24
|
+
tasks.push(
|
|
25
|
+
`Merge any remaining user-authored content from \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` into \`${AGENT_INSTRUCTIONS_FILE}\`, then delete the legacy file`,
|
|
26
|
+
);
|
|
27
|
+
} else if (hasLegacyInstructions) {
|
|
28
|
+
tasks.push(
|
|
29
|
+
`Rename \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` to \`${AGENT_INSTRUCTIONS_FILE}\` to use the canonical agent instructions filename`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const instructionsPath = repo.agentInstructionsPath() ?? AGENT_INSTRUCTIONS_FILE;
|
|
34
|
+
if (!repo.hasAgentInstructionsMarkers()) {
|
|
35
|
+
tasks.push(
|
|
36
|
+
`\`${instructionsPath}\` exists but is missing framework markers — add \`<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\` and \`<!-- END CONTEXT-TREE FRAMEWORK -->\` sections`,
|
|
37
|
+
);
|
|
38
|
+
} else {
|
|
39
|
+
const text = repo.readAgentInstructions() ?? "";
|
|
40
|
+
const afterMarker = text.split(FRAMEWORK_END_MARKER);
|
|
41
|
+
if (afterMarker.length > 1) {
|
|
42
|
+
const userSection = afterMarker[1].trim();
|
|
43
|
+
const lines = userSection
|
|
44
|
+
.split("\n")
|
|
45
|
+
.filter(
|
|
46
|
+
(l) =>
|
|
47
|
+
l.trim() &&
|
|
48
|
+
!l.trim().startsWith("#") &&
|
|
49
|
+
!l.trim().startsWith("<!--"),
|
|
50
|
+
);
|
|
51
|
+
if (lines.length === 0) {
|
|
52
|
+
tasks.push(
|
|
53
|
+
`Add your project-specific instructions below the framework markers in ${AGENT_INSTRUCTIONS_FILE}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { group: "Agent Instructions", order: 3, tasks };
|
|
59
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { FRAMEWORK_EXAMPLES_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (repo.pathExists(".claude/settings.json")) {
|
|
8
|
+
if (!repo.fileContains(".claude/settings.json", "inject-tree-context")) {
|
|
9
|
+
tasks.push(
|
|
10
|
+
`Add SessionStart hook to \`.claude/settings.json\` (see \`${FRAMEWORK_EXAMPLES_DIR}/claude-code/\`)`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
} else if (!repo.anyAgentConfig()) {
|
|
14
|
+
tasks.push(
|
|
15
|
+
`No agent configuration detected. Configure your agent to load tree context at session start. See \`${FRAMEWORK_EXAMPLES_DIR}/\` for supported agents. You can skip this and set it up later.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return { group: "Agent Integration", order: 5, tasks };
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { INTERACTIVE_TOOL } from "#skill/engine/init.js";
|
|
4
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
5
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
6
|
+
import { FRAMEWORK_WORKFLOWS_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
7
|
+
|
|
8
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
9
|
+
const tasks: string[] = [];
|
|
10
|
+
let hasValidation = false;
|
|
11
|
+
let hasPrReview = false;
|
|
12
|
+
let hasCodeowners = false;
|
|
13
|
+
const workflowsDir = join(repo.root, ".github", "workflows");
|
|
14
|
+
try {
|
|
15
|
+
if (statSync(workflowsDir).isDirectory()) {
|
|
16
|
+
for (const name of readdirSync(workflowsDir)) {
|
|
17
|
+
if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue;
|
|
18
|
+
const fullPath = join(workflowsDir, name);
|
|
19
|
+
try {
|
|
20
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
21
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
22
|
+
if (
|
|
23
|
+
content.includes("validate_nodes") ||
|
|
24
|
+
content.includes("validate_members")
|
|
25
|
+
) {
|
|
26
|
+
hasValidation = true;
|
|
27
|
+
}
|
|
28
|
+
if (content.includes("run-review")) {
|
|
29
|
+
hasPrReview = true;
|
|
30
|
+
}
|
|
31
|
+
if (content.includes("generate-codeowners")) {
|
|
32
|
+
hasCodeowners = true;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// workflows dir doesn't exist
|
|
41
|
+
}
|
|
42
|
+
if (!hasValidation) {
|
|
43
|
+
tasks.push(
|
|
44
|
+
`No validation workflow found — copy \`${FRAMEWORK_WORKFLOWS_DIR}/validate.yml\` to \`.github/workflows/validate.yml\``,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (!hasPrReview) {
|
|
48
|
+
tasks.push(
|
|
49
|
+
`Use ${INTERACTIVE_TOOL} to ask whether the user wants AI-powered PR reviews. Options:\n` +
|
|
50
|
+
" 1. **OpenRouter** — use an OpenRouter API key\n" +
|
|
51
|
+
" 2. **Claude API** — use a Claude API key directly\n" +
|
|
52
|
+
" 3. **Skip** — do not set up PR reviews\n" +
|
|
53
|
+
`If (1): copy \`${FRAMEWORK_WORKFLOWS_DIR}/pr-review.yml\` to \`.github/workflows/pr-review.yml\` as-is; the repo secret name is \`OPENROUTER_API_KEY\`. ` +
|
|
54
|
+
"If (2): copy the workflow and replace the `env` block with `ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}`, remove the `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_DEFAULT_SONNET_MODEL` lines; the repo secret name is `ANTHROPIC_API_KEY`. " +
|
|
55
|
+
"If (3): skip this and the next task.",
|
|
56
|
+
);
|
|
57
|
+
tasks.push(
|
|
58
|
+
`Use ${INTERACTIVE_TOOL} to ask how the user wants to configure the API secret. Options:\n` +
|
|
59
|
+
" 1. **Set it now** — provide the key and the agent will run `gh secret set <SECRET_NAME> --body <KEY>`\n" +
|
|
60
|
+
" 2. **I'll do it myself** — the agent will show manual instructions\n" +
|
|
61
|
+
"If (1): ask the user to provide the key, then run `gh secret set` with the secret name from the previous step. " +
|
|
62
|
+
"If (2): tell the user to go to their repo → Settings → Secrets and variables → Actions → New repository secret, and create the secret with the name from the previous step. " +
|
|
63
|
+
"Skip this task if the user chose Skip in the previous step.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!hasCodeowners) {
|
|
67
|
+
tasks.push(
|
|
68
|
+
`No CODEOWNERS workflow found — copy \`${FRAMEWORK_WORKFLOWS_DIR}/codeowners.yml\` to \`.github/workflows/codeowners.yml\` to auto-generate CODEOWNERS from tree ownership on every PR.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { group: "CI / Validation", order: 6, tasks };
|
|
72
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { SKILL_ROOT } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (!repo.hasFramework()) {
|
|
8
|
+
tasks.push(
|
|
9
|
+
`\`${SKILL_ROOT}/\` not found — run \`context-tree init\` to install the framework skill bundled with the current \`first-tree\` package`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return { group: "Framework", order: 1, tasks };
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import * as agentInstructions from "#skill/engine/rules/agent-instructions.js";
|
|
3
|
+
import * as agentIntegration from "#skill/engine/rules/agent-integration.js";
|
|
4
|
+
import * as ciValidation from "#skill/engine/rules/ci-validation.js";
|
|
5
|
+
import * as framework from "#skill/engine/rules/framework.js";
|
|
6
|
+
import * as members from "#skill/engine/rules/members.js";
|
|
7
|
+
import * as populateTree from "#skill/engine/rules/populate-tree.js";
|
|
8
|
+
import * as rootNode from "#skill/engine/rules/root-node.js";
|
|
9
|
+
|
|
10
|
+
export interface RuleResult {
|
|
11
|
+
group: string;
|
|
12
|
+
order: number;
|
|
13
|
+
tasks: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Rule {
|
|
17
|
+
evaluate(repo: Repo): RuleResult;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ALL_RULES: Rule[] = [
|
|
21
|
+
framework,
|
|
22
|
+
rootNode,
|
|
23
|
+
agentInstructions,
|
|
24
|
+
members,
|
|
25
|
+
agentIntegration,
|
|
26
|
+
ciValidation,
|
|
27
|
+
populateTree,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function evaluateAll(repo: Repo): RuleResult[] {
|
|
31
|
+
const results: RuleResult[] = [];
|
|
32
|
+
for (const rule of ALL_RULES) {
|
|
33
|
+
const result = rule.evaluate(repo);
|
|
34
|
+
if (result.tasks.length > 0) {
|
|
35
|
+
results.push(result);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return results.sort((a, b) => a.order - b.order);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { framework, rootNode, agentInstructions, members, agentIntegration, ciValidation, populateTree };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
|
|
4
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
5
|
+
const tasks: string[] = [];
|
|
6
|
+
if (!repo.pathExists("members")) {
|
|
7
|
+
tasks.push("`members/` directory is missing — create it with a NODE.md");
|
|
8
|
+
} else if (!repo.pathExists("members/NODE.md")) {
|
|
9
|
+
tasks.push("`members/NODE.md` is missing — create it from the template");
|
|
10
|
+
}
|
|
11
|
+
if (repo.hasMembers() && repo.memberCount() === 0) {
|
|
12
|
+
tasks.push(
|
|
13
|
+
"Add at least one member node under `members/`. Analyze the user's code repositories (git history, CODEOWNERS, README contributors) to suggest members, then confirm with the user",
|
|
14
|
+
);
|
|
15
|
+
} else if (!repo.hasMembers()) {
|
|
16
|
+
tasks.push(
|
|
17
|
+
"Add at least one member node under `members/`. Analyze the user's code repositories (git history, CODEOWNERS, README contributors) to suggest members, then confirm with the user",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return { group: "Members", order: 4, tasks };
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { INTERACTIVE_TOOL } from "#skill/engine/init.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
|
|
8
|
+
tasks.push(
|
|
9
|
+
`Ask the user whether they want to populate the full context tree now using the **${INTERACTIVE_TOOL}** tool. ` +
|
|
10
|
+
"Present two options: (1) **Yes — populate the full tree**: the agent will analyze source repositories, " +
|
|
11
|
+
"create sub-domains, and populate NODE.md files for each domain and sub-domain; " +
|
|
12
|
+
"(2) **No — I'll do it later**: skip deep population and finish init with just the top-level structure. " +
|
|
13
|
+
"If the user selects No, check off all remaining items in this section and move on.",
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
tasks.push(
|
|
17
|
+
"If the user selected Yes: analyze the codebase (and any additional repositories the user provides) to identify " +
|
|
18
|
+
"logical sub-domains within each top-level domain. For each sub-domain, create a directory with a NODE.md " +
|
|
19
|
+
"containing proper frontmatter (title, owners) and a description of the sub-domain's purpose, boundaries, " +
|
|
20
|
+
"and key decisions. Create deeper sub-domains when a domain is large enough to warrant further decomposition.",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
tasks.push(
|
|
24
|
+
"Use **sub-tasks** (TaskCreate) to parallelize the population work — create one sub-task per top-level domain " +
|
|
25
|
+
"so each domain can be populated concurrently. Each sub-task should: read the relevant source code, identify " +
|
|
26
|
+
"sub-domains, create NODE.md files, and establish soft_links between related domains.",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
tasks.push(
|
|
30
|
+
"After all domains are populated, update the root NODE.md to list every top-level domain with a one-line " +
|
|
31
|
+
"description. Ensure all NODE.md files pass `context-tree verify` — valid frontmatter, no placeholders, " +
|
|
32
|
+
"and soft_links that resolve correctly.",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return { group: "Populate Tree", order: 7, tasks };
|
|
36
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { FRAMEWORK_TEMPLATES_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (!repo.pathExists("NODE.md")) {
|
|
8
|
+
tasks.push(
|
|
9
|
+
`NODE.md is missing — create from \`${FRAMEWORK_TEMPLATES_DIR}/root-node.md.template\`. ` +
|
|
10
|
+
"Ask the user for their code repositories or project directories, then analyze the source to determine the project description and domain structure",
|
|
11
|
+
);
|
|
12
|
+
} else {
|
|
13
|
+
const fm = repo.frontmatter("NODE.md");
|
|
14
|
+
if (fm === null) {
|
|
15
|
+
tasks.push(
|
|
16
|
+
"NODE.md exists but has no frontmatter — add frontmatter with title and owners fields",
|
|
17
|
+
);
|
|
18
|
+
} else {
|
|
19
|
+
if (!fm.title || fm.title.startsWith("<")) {
|
|
20
|
+
tasks.push(
|
|
21
|
+
"NODE.md has a placeholder title — replace with your organization name",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (
|
|
25
|
+
!fm.owners ||
|
|
26
|
+
fm.owners.length === 0 ||
|
|
27
|
+
(fm.owners.length === 1 && fm.owners[0].startsWith("<"))
|
|
28
|
+
) {
|
|
29
|
+
tasks.push(
|
|
30
|
+
"NODE.md has placeholder owners — set owners to your GitHub username(s)",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (repo.hasPlaceholderNode()) {
|
|
35
|
+
tasks.push(
|
|
36
|
+
"NODE.md has placeholder content — ask the user for their code repositories or project directories, then analyze the source to fill in the project description and domain structure",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { group: "Root Node", order: 2, tasks };
|
|
41
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
FRAMEWORK_EXAMPLES_DIR,
|
|
4
|
+
FRAMEWORK_HELPERS_DIR,
|
|
5
|
+
LEGACY_SKILL_EXAMPLES_DIR,
|
|
6
|
+
LEGACY_EXAMPLES_DIR,
|
|
7
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
8
|
+
|
|
9
|
+
export const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
10
|
+
export const CODEX_CONFIG_PATH = ".codex/config.json";
|
|
11
|
+
|
|
12
|
+
export function claudeCodeExampleCandidates(): string[] {
|
|
13
|
+
return [
|
|
14
|
+
join(FRAMEWORK_EXAMPLES_DIR, "claude-code"),
|
|
15
|
+
join(LEGACY_SKILL_EXAMPLES_DIR, "claude-code"),
|
|
16
|
+
join(LEGACY_EXAMPLES_DIR, "claude-code"),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function injectTreeContextHint(): string {
|
|
21
|
+
return join(FRAMEWORK_HELPERS_DIR, "inject-tree-context.sh");
|
|
22
|
+
}
|