first-tree 0.0.3 → 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 +69 -27
- package/dist/cli.js +28 -13
- package/dist/{help-xEI-s9iN.js → help-Dtdj91HJ.js} +1 -1
- package/dist/{init-DtOjj0wc.js → init--VepFe6N.js} +171 -21
- package/dist/{installer-rcZpGLnM.js → installer-cH7N4RNj.js} +2 -2
- package/dist/onboarding-C9cYSE6F.js +2 -0
- package/dist/onboarding-CPP8fF4D.js +10 -0
- package/dist/{repo-BTJG8BU1.js → repo-DY57bMqr.js} +143 -12
- package/dist/{upgrade-COGgI7Rj.js → upgrade-Cgx_K2HM.js} +46 -7
- package/dist/{verify-CxN6JiV9.js → verify-mC9ZTd1f.js} +66 -6
- package/package.json +1 -1
- package/skills/first-tree/SKILL.md +8 -4
- package/skills/first-tree/assets/framework/VERSION +1 -1
- package/skills/first-tree/assets/framework/helpers/run-review.ts +16 -2
- package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +1 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +6 -3
- package/skills/first-tree/engine/commands/init.ts +1 -1
- package/skills/first-tree/engine/commands/upgrade.ts +1 -1
- package/skills/first-tree/engine/commands/verify.ts +1 -1
- package/skills/first-tree/engine/init.ts +285 -16
- package/skills/first-tree/engine/repo.ts +185 -9
- package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
- package/skills/first-tree/engine/runtime/asset-loader.ts +7 -0
- package/skills/first-tree/engine/upgrade.ts +66 -9
- package/skills/first-tree/engine/validators/nodes.ts +48 -3
- package/skills/first-tree/engine/verify.ts +61 -3
- package/skills/first-tree/references/maintainer-architecture.md +1 -1
- package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
- package/skills/first-tree/references/onboarding.md +32 -9
- package/skills/first-tree/references/source-map.md +3 -3
- package/skills/first-tree/references/upgrade-contract.md +14 -5
- package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
- package/skills/first-tree/tests/helpers.ts +24 -4
- package/skills/first-tree/tests/init.test.ts +103 -6
- package/skills/first-tree/tests/repo.test.ts +87 -9
- package/skills/first-tree/tests/rules.test.ts +26 -7
- package/skills/first-tree/tests/skill-artifacts.test.ts +4 -0
- package/skills/first-tree/tests/thin-cli.test.ts +52 -7
- package/skills/first-tree/tests/upgrade.test.ts +19 -5
- package/skills/first-tree/tests/verify.test.ts +106 -7
- package/dist/onboarding-6Fr5Gkrk.js +0 -2
- package/dist/onboarding-B9zPGvvG.js +0 -10
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
mkdirSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
statSync,
|
|
4
7
|
writeFileSync,
|
|
5
8
|
} from "node:fs";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
9
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
7
10
|
import { Repo } from "#skill/engine/repo.js";
|
|
8
11
|
import { ONBOARDING_TEXT } from "#skill/engine/onboarding.js";
|
|
9
12
|
import { evaluateAll } from "#skill/engine/rules/index.js";
|
|
@@ -14,9 +17,12 @@ import {
|
|
|
14
17
|
resolveBundledPackageRoot,
|
|
15
18
|
} from "#skill/engine/runtime/installer.js";
|
|
16
19
|
import {
|
|
20
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
21
|
+
AGENT_INSTRUCTIONS_TEMPLATE,
|
|
17
22
|
FRAMEWORK_ASSET_ROOT,
|
|
18
23
|
FRAMEWORK_VERSION,
|
|
19
24
|
INSTALLED_PROGRESS,
|
|
25
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
20
26
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
21
27
|
|
|
22
28
|
/**
|
|
@@ -25,13 +31,39 @@ import {
|
|
|
25
31
|
* all generated task text at once.
|
|
26
32
|
*/
|
|
27
33
|
export const INTERACTIVE_TOOL = "AskUserQuestion";
|
|
34
|
+
export const INIT_USAGE = `usage: context-tree init [--here] [--tree-name NAME] [--tree-path PATH]
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
By default, running \`context-tree init\` inside a source or workspace repo creates
|
|
37
|
+
a sibling dedicated tree repo named \`<repo>-context\`.
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--here Initialize the current repo in place
|
|
41
|
+
--tree-name NAME Name the dedicated sibling tree repo to create
|
|
42
|
+
--tree-path PATH Use an explicit tree repo path
|
|
43
|
+
--help Show this help message
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
interface TemplateTarget {
|
|
47
|
+
templateName: string;
|
|
48
|
+
targetPath: string;
|
|
49
|
+
skipIfExists?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const TEMPLATE_MAP: TemplateTarget[] = [
|
|
53
|
+
{ templateName: "root-node.md.template", targetPath: "NODE.md" },
|
|
54
|
+
{
|
|
55
|
+
templateName: AGENT_INSTRUCTIONS_TEMPLATE,
|
|
56
|
+
targetPath: AGENT_INSTRUCTIONS_FILE,
|
|
57
|
+
skipIfExists: [AGENT_INSTRUCTIONS_FILE, LEGACY_AGENT_INSTRUCTIONS_FILE],
|
|
58
|
+
},
|
|
59
|
+
{ templateName: "members-domain.md.template", targetPath: "members/NODE.md" },
|
|
33
60
|
];
|
|
34
61
|
|
|
62
|
+
interface TaskListContext {
|
|
63
|
+
sourceRepoPath?: string;
|
|
64
|
+
dedicatedTreeRepo?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
35
67
|
function installSkill(source: string, target: string): void {
|
|
36
68
|
copyCanonicalSkill(source, target);
|
|
37
69
|
console.log(
|
|
@@ -41,18 +73,46 @@ function installSkill(source: string, target: string): void {
|
|
|
41
73
|
|
|
42
74
|
function renderTemplates(target: string): void {
|
|
43
75
|
const frameworkDir = join(target, FRAMEWORK_ASSET_ROOT);
|
|
44
|
-
for (const
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
for (const { templateName, targetPath, skipIfExists } of TEMPLATE_MAP) {
|
|
77
|
+
const existingPaths = skipIfExists ?? [targetPath];
|
|
78
|
+
const existingPath = existingPaths.find((candidate) =>
|
|
79
|
+
existsSync(join(target, candidate)),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (existingPath !== undefined) {
|
|
83
|
+
console.log(` Skipped ${targetPath} (found existing ${existingPath})`);
|
|
47
84
|
} else if (renderTemplateFile(frameworkDir, templateName, target, targetPath)) {
|
|
48
85
|
console.log(` Created ${targetPath}`);
|
|
49
86
|
}
|
|
50
87
|
}
|
|
51
88
|
}
|
|
52
89
|
|
|
53
|
-
export function formatTaskList(
|
|
90
|
+
export function formatTaskList(
|
|
91
|
+
groups: RuleResult[],
|
|
92
|
+
context?: TaskListContext,
|
|
93
|
+
): string {
|
|
54
94
|
const lines: string[] = [
|
|
55
95
|
"# Context Tree Init\n",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
if (context?.dedicatedTreeRepo) {
|
|
99
|
+
lines.push(
|
|
100
|
+
"This repository is the dedicated Context Tree. Keep decisions, rationale," +
|
|
101
|
+
" cross-domain relationships, and ownership here; keep execution detail" +
|
|
102
|
+
" in your source repositories.",
|
|
103
|
+
"",
|
|
104
|
+
);
|
|
105
|
+
if (context.sourceRepoPath) {
|
|
106
|
+
lines.push(`**Bootstrap source repo:** \`${context.sourceRepoPath}\``, "");
|
|
107
|
+
}
|
|
108
|
+
lines.push(
|
|
109
|
+
"When you publish this tree repo, keep it in the same GitHub organization" +
|
|
110
|
+
" as the source repo unless you have a reason not to.",
|
|
111
|
+
"",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lines.push(
|
|
56
116
|
"**Agent instructions:** Before starting work, analyze the full task list below and" +
|
|
57
117
|
" identify all information you need from the user. Ask the user for their code" +
|
|
58
118
|
" repositories or project directories so you can analyze the source yourself —" +
|
|
@@ -61,7 +121,7 @@ export function formatTaskList(groups: RuleResult[]): string {
|
|
|
61
121
|
` **${INTERACTIVE_TOOL}** tool with structured options — present selectable choices` +
|
|
62
122
|
" (with label and description) so the user can pick instead of typing free-form" +
|
|
63
123
|
` answers. You may batch up to 4 questions per ${INTERACTIVE_TOOL} call.\n`,
|
|
64
|
-
|
|
124
|
+
);
|
|
65
125
|
for (const group of groups) {
|
|
66
126
|
lines.push(`## ${group.group}`);
|
|
67
127
|
for (const task of group.tasks) {
|
|
@@ -75,7 +135,9 @@ export function formatTaskList(groups: RuleResult[]): string {
|
|
|
75
135
|
);
|
|
76
136
|
lines.push(`- [ ] \`${FRAMEWORK_VERSION}\` exists`);
|
|
77
137
|
lines.push("- [ ] Root NODE.md has valid frontmatter (title, owners)");
|
|
78
|
-
lines.push(
|
|
138
|
+
lines.push(
|
|
139
|
+
`- [ ] \`${AGENT_INSTRUCTIONS_FILE}\` is the only agent instructions file and has framework markers`,
|
|
140
|
+
);
|
|
79
141
|
lines.push("- [ ] `context-tree verify` passes with no errors");
|
|
80
142
|
lines.push("- [ ] At least one member node exists");
|
|
81
143
|
lines.push("");
|
|
@@ -99,17 +161,42 @@ export function writeProgress(repo: Repo, content: string): void {
|
|
|
99
161
|
|
|
100
162
|
export interface InitOptions {
|
|
101
163
|
sourceRoot?: string;
|
|
164
|
+
here?: boolean;
|
|
165
|
+
treeName?: string;
|
|
166
|
+
treePath?: string;
|
|
167
|
+
currentCwd?: string;
|
|
168
|
+
gitInitializer?: (root: string) => void;
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
export function runInit(repo?: Repo, options?: InitOptions): number {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (
|
|
172
|
+
const sourceRepo = repo ?? new Repo();
|
|
173
|
+
const initTarget = resolveInitTarget(sourceRepo, options);
|
|
174
|
+
if (initTarget.ok === false) {
|
|
108
175
|
console.error(
|
|
109
|
-
|
|
176
|
+
`Error: ${initTarget.message}`,
|
|
110
177
|
);
|
|
111
178
|
return 1;
|
|
112
179
|
}
|
|
180
|
+
const r = initTarget.repo;
|
|
181
|
+
const taskListContext = initTarget.dedicatedTreeRepo
|
|
182
|
+
? {
|
|
183
|
+
dedicatedTreeRepo: true,
|
|
184
|
+
sourceRepoPath: relativePathFrom(r.root, sourceRepo.root),
|
|
185
|
+
}
|
|
186
|
+
: undefined;
|
|
187
|
+
|
|
188
|
+
if (initTarget.dedicatedTreeRepo) {
|
|
189
|
+
console.log(
|
|
190
|
+
"Recommended workflow: keep the Context Tree in a dedicated repo separate" +
|
|
191
|
+
" from your source/workspace repo.",
|
|
192
|
+
);
|
|
193
|
+
console.log(` Source repo: ${sourceRepo.root}`);
|
|
194
|
+
console.log(` Tree repo: ${r.root}`);
|
|
195
|
+
if (initTarget.createdGitRepo) {
|
|
196
|
+
console.log(" Initialized a new git repo for the tree.");
|
|
197
|
+
}
|
|
198
|
+
console.log();
|
|
199
|
+
}
|
|
113
200
|
|
|
114
201
|
if (!r.hasFramework()) {
|
|
115
202
|
try {
|
|
@@ -137,9 +224,191 @@ export function runInit(repo?: Repo, options?: InitOptions): number {
|
|
|
137
224
|
return 0;
|
|
138
225
|
}
|
|
139
226
|
|
|
140
|
-
const output = formatTaskList(groups);
|
|
227
|
+
const output = formatTaskList(groups, taskListContext);
|
|
141
228
|
console.log(output);
|
|
142
229
|
writeProgress(r, output);
|
|
143
230
|
console.log(`Progress file written to ${r.preferredProgressPath()}`);
|
|
231
|
+
if (initTarget.dedicatedTreeRepo) {
|
|
232
|
+
console.log(
|
|
233
|
+
`Continue in ${relativePathFrom(sourceRepo.root, r.root)} and keep your source repos available as additional working directories when you populate the tree.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
144
236
|
return 0;
|
|
145
237
|
}
|
|
238
|
+
|
|
239
|
+
export interface ParsedInitArgs {
|
|
240
|
+
here?: boolean;
|
|
241
|
+
treeName?: string;
|
|
242
|
+
treePath?: string;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function parseInitArgs(
|
|
246
|
+
args: string[],
|
|
247
|
+
): ParsedInitArgs | { error: string } {
|
|
248
|
+
const parsed: ParsedInitArgs = {};
|
|
249
|
+
|
|
250
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
251
|
+
const arg = args[index];
|
|
252
|
+
switch (arg) {
|
|
253
|
+
case "--here":
|
|
254
|
+
parsed.here = true;
|
|
255
|
+
break;
|
|
256
|
+
case "--tree-name": {
|
|
257
|
+
const value = args[index + 1];
|
|
258
|
+
if (!value) {
|
|
259
|
+
return { error: "Missing value for --tree-name" };
|
|
260
|
+
}
|
|
261
|
+
parsed.treeName = value;
|
|
262
|
+
index += 1;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "--tree-path": {
|
|
266
|
+
const value = args[index + 1];
|
|
267
|
+
if (!value) {
|
|
268
|
+
return { error: "Missing value for --tree-path" };
|
|
269
|
+
}
|
|
270
|
+
parsed.treePath = value;
|
|
271
|
+
index += 1;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
default:
|
|
275
|
+
return { error: `Unknown init option: ${arg}` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (parsed.here && parsed.treeName) {
|
|
280
|
+
return { error: "Cannot combine --here with --tree-name" };
|
|
281
|
+
}
|
|
282
|
+
if (parsed.here && parsed.treePath) {
|
|
283
|
+
return { error: "Cannot combine --here with --tree-path" };
|
|
284
|
+
}
|
|
285
|
+
if (parsed.treeName && parsed.treePath) {
|
|
286
|
+
return { error: "Cannot combine --tree-name with --tree-path" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return parsed;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function runInitCli(args: string[] = []): number {
|
|
293
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
294
|
+
console.log(INIT_USAGE);
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const parsed = parseInitArgs(args);
|
|
299
|
+
if ("error" in parsed) {
|
|
300
|
+
console.error(parsed.error);
|
|
301
|
+
console.log(INIT_USAGE);
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return runInit(undefined, parsed);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
interface ResolvedInitTarget {
|
|
309
|
+
ok: true;
|
|
310
|
+
createdGitRepo: boolean;
|
|
311
|
+
dedicatedTreeRepo: boolean;
|
|
312
|
+
repo: Repo;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
interface FailedInitTarget {
|
|
316
|
+
message: string;
|
|
317
|
+
ok: false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function resolveInitTarget(
|
|
321
|
+
sourceRepo: Repo,
|
|
322
|
+
options?: InitOptions,
|
|
323
|
+
): FailedInitTarget | ResolvedInitTarget {
|
|
324
|
+
if (!sourceRepo.isGitRepo()) {
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
message:
|
|
328
|
+
"not a git repository. Run this from your source/workspace repo, or create a dedicated tree repo first:\n git init\n context-tree init --here",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const targetRoot = determineTargetRoot(sourceRepo, options);
|
|
333
|
+
const dedicatedTreeRepo = targetRoot !== sourceRepo.root;
|
|
334
|
+
let createdGitRepo = false;
|
|
335
|
+
try {
|
|
336
|
+
createdGitRepo = ensureGitRepo(targetRoot, options?.gitInitializer);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
message,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
createdGitRepo,
|
|
348
|
+
dedicatedTreeRepo,
|
|
349
|
+
repo: new Repo(targetRoot),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function determineTargetRoot(sourceRepo: Repo, options?: InitOptions): string {
|
|
354
|
+
if (options?.treePath) {
|
|
355
|
+
return resolve(options.currentCwd ?? process.cwd(), options.treePath);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (options?.here) {
|
|
359
|
+
return sourceRepo.root;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (options?.treeName) {
|
|
363
|
+
return join(dirname(sourceRepo.root), options.treeName);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
sourceRepo.looksLikeTreeRepo()
|
|
368
|
+
|| sourceRepo.isLikelyEmptyRepo()
|
|
369
|
+
|| !sourceRepo.isLikelySourceRepo()
|
|
370
|
+
) {
|
|
371
|
+
return sourceRepo.root;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return join(dirname(sourceRepo.root), `${sourceRepo.repoName()}-context`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function ensureGitRepo(
|
|
378
|
+
targetRoot: string,
|
|
379
|
+
gitInitializer?: (root: string) => void,
|
|
380
|
+
): boolean {
|
|
381
|
+
if (existsSync(targetRoot)) {
|
|
382
|
+
if (!statSync(targetRoot).isDirectory()) {
|
|
383
|
+
throw new Error(`Target path is not a directory: ${targetRoot}`);
|
|
384
|
+
}
|
|
385
|
+
if (new Repo(targetRoot).isGitRepo()) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (readdirSync(targetRoot).length !== 0) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Target path exists and is not a git repository: ${targetRoot}. Run \`git init\` there first or choose a different tree path.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
(gitInitializer ?? defaultGitInitializer)(targetRoot);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function defaultGitInitializer(root: string): void {
|
|
402
|
+
execFileSync("git", ["init"], {
|
|
403
|
+
cwd: root,
|
|
404
|
+
stdio: "ignore",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function relativePathFrom(from: string, to: string): string {
|
|
409
|
+
const rel = relative(from, to);
|
|
410
|
+
if (rel === "") {
|
|
411
|
+
return ".";
|
|
412
|
+
}
|
|
413
|
+
return rel.startsWith("..") ? rel : `./${rel}`;
|
|
414
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
4
5
|
FRAMEWORK_VERSION,
|
|
5
6
|
LEGACY_SKILL_PROGRESS,
|
|
6
7
|
LEGACY_SKILL_VERSION,
|
|
8
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
7
9
|
LEGACY_PROGRESS,
|
|
8
10
|
LEGACY_VERSION,
|
|
9
11
|
INSTALLED_PROGRESS,
|
|
12
|
+
agentInstructionsFileCandidates,
|
|
10
13
|
type FrameworkLayout,
|
|
11
14
|
detectFrameworkLayout,
|
|
12
15
|
frameworkVersionCandidates,
|
|
@@ -17,6 +20,59 @@ import {
|
|
|
17
20
|
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
18
21
|
const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
|
|
19
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
|
+
]);
|
|
20
76
|
|
|
21
77
|
export const FRAMEWORK_BEGIN_MARKER = "<!-- BEGIN CONTEXT-TREE FRAMEWORK";
|
|
22
78
|
export const FRAMEWORK_END_MARKER = "<!-- END CONTEXT-TREE FRAMEWORK -->";
|
|
@@ -26,11 +82,35 @@ export interface Frontmatter {
|
|
|
26
82
|
owners?: string[];
|
|
27
83
|
}
|
|
28
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
|
+
|
|
29
108
|
export class Repo {
|
|
30
109
|
readonly root: string;
|
|
31
110
|
|
|
32
111
|
constructor(root?: string) {
|
|
33
|
-
|
|
112
|
+
const start = resolve(root ?? process.cwd());
|
|
113
|
+
this.root = root === undefined ? discoverGitRoot(start) ?? start : start;
|
|
34
114
|
}
|
|
35
115
|
|
|
36
116
|
pathExists(relPath: string): boolean {
|
|
@@ -85,11 +165,7 @@ export class Repo {
|
|
|
85
165
|
}
|
|
86
166
|
|
|
87
167
|
isGitRepo(): boolean {
|
|
88
|
-
|
|
89
|
-
return statSync(join(this.root, ".git")).isDirectory();
|
|
90
|
-
} catch {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
168
|
+
return hasGitMetadata(this.root);
|
|
93
169
|
}
|
|
94
170
|
|
|
95
171
|
hasFramework(): boolean {
|
|
@@ -136,8 +212,30 @@ export class Repo {
|
|
|
136
212
|
return FRAMEWORK_VERSION;
|
|
137
213
|
}
|
|
138
214
|
|
|
139
|
-
|
|
140
|
-
|
|
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();
|
|
141
239
|
if (text === null) return false;
|
|
142
240
|
return text.includes(FRAMEWORK_BEGIN_MARKER) && text.includes(FRAMEWORK_END_MARKER);
|
|
143
241
|
}
|
|
@@ -181,4 +279,82 @@ export class Repo {
|
|
|
181
279
|
hasPlaceholderNode(): boolean {
|
|
182
280
|
return this.fileContains("NODE.md", "<!-- PLACEHOLDER");
|
|
183
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
|
+
}
|
|
184
360
|
}
|
|
@@ -1,20 +1,42 @@
|
|
|
1
1
|
import { FRAMEWORK_END_MARKER } from "#skill/engine/repo.js";
|
|
2
2
|
import type { Repo } from "#skill/engine/repo.js";
|
|
3
3
|
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
4
|
-
import {
|
|
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";
|
|
5
10
|
|
|
6
11
|
export function evaluate(repo: Repo): RuleResult {
|
|
7
12
|
const tasks: string[] = [];
|
|
8
|
-
|
|
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) {
|
|
9
24
|
tasks.push(
|
|
10
|
-
`
|
|
25
|
+
`Merge any remaining user-authored content from \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` into \`${AGENT_INSTRUCTIONS_FILE}\`, then delete the legacy file`,
|
|
11
26
|
);
|
|
12
|
-
} else if (
|
|
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()) {
|
|
13
35
|
tasks.push(
|
|
14
|
-
|
|
36
|
+
`\`${instructionsPath}\` exists but is missing framework markers — add \`<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\` and \`<!-- END CONTEXT-TREE FRAMEWORK -->\` sections`,
|
|
15
37
|
);
|
|
16
38
|
} else {
|
|
17
|
-
const text = repo.
|
|
39
|
+
const text = repo.readAgentInstructions() ?? "";
|
|
18
40
|
const afterMarker = text.split(FRAMEWORK_END_MARKER);
|
|
19
41
|
if (afterMarker.length > 1) {
|
|
20
42
|
const userSection = afterMarker[1].trim();
|
|
@@ -28,7 +50,7 @@ export function evaluate(repo: Repo): RuleResult {
|
|
|
28
50
|
);
|
|
29
51
|
if (lines.length === 0) {
|
|
30
52
|
tasks.push(
|
|
31
|
-
|
|
53
|
+
`Add your project-specific instructions below the framework markers in ${AGENT_INSTRUCTIONS_FILE}`,
|
|
32
54
|
);
|
|
33
55
|
}
|
|
34
56
|
}
|
|
@@ -14,6 +14,9 @@ export const FRAMEWORK_PROMPTS_DIR = join(FRAMEWORK_ASSET_ROOT, "prompts");
|
|
|
14
14
|
export const FRAMEWORK_EXAMPLES_DIR = join(FRAMEWORK_ASSET_ROOT, "examples");
|
|
15
15
|
export const FRAMEWORK_HELPERS_DIR = join(FRAMEWORK_ASSET_ROOT, "helpers");
|
|
16
16
|
export const INSTALLED_PROGRESS = join(SKILL_ROOT, "progress.md");
|
|
17
|
+
export const AGENT_INSTRUCTIONS_FILE = "AGENTS.md";
|
|
18
|
+
export const LEGACY_AGENT_INSTRUCTIONS_FILE = "AGENT.md";
|
|
19
|
+
export const AGENT_INSTRUCTIONS_TEMPLATE = "agents.md.template";
|
|
17
20
|
|
|
18
21
|
export const LEGACY_SKILL_NAME = "first-tree-cli-framework";
|
|
19
22
|
export const LEGACY_SKILL_ROOT = join("skills", LEGACY_SKILL_NAME);
|
|
@@ -68,6 +71,10 @@ export function progressFileCandidates(): string[] {
|
|
|
68
71
|
return [INSTALLED_PROGRESS, LEGACY_SKILL_PROGRESS, LEGACY_PROGRESS];
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
export function agentInstructionsFileCandidates(): string[] {
|
|
75
|
+
return [AGENT_INSTRUCTIONS_FILE, LEGACY_AGENT_INSTRUCTIONS_FILE];
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
export function frameworkTemplateDirCandidates(): string[] {
|
|
72
79
|
return [
|
|
73
80
|
FRAMEWORK_TEMPLATES_DIR,
|