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.
Files changed (43) hide show
  1. package/README.md +69 -27
  2. package/dist/cli.js +28 -13
  3. package/dist/{help-xEI-s9iN.js → help-Dtdj91HJ.js} +1 -1
  4. package/dist/{init-DtOjj0wc.js → init--VepFe6N.js} +171 -21
  5. package/dist/{installer-rcZpGLnM.js → installer-cH7N4RNj.js} +2 -2
  6. package/dist/onboarding-C9cYSE6F.js +2 -0
  7. package/dist/onboarding-CPP8fF4D.js +10 -0
  8. package/dist/{repo-BTJG8BU1.js → repo-DY57bMqr.js} +143 -12
  9. package/dist/{upgrade-COGgI7Rj.js → upgrade-Cgx_K2HM.js} +46 -7
  10. package/dist/{verify-CxN6JiV9.js → verify-mC9ZTd1f.js} +66 -6
  11. package/package.json +1 -1
  12. package/skills/first-tree/SKILL.md +8 -4
  13. package/skills/first-tree/assets/framework/VERSION +1 -1
  14. package/skills/first-tree/assets/framework/helpers/run-review.ts +16 -2
  15. package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +1 -0
  16. package/skills/first-tree/assets/framework/templates/root-node.md.template +6 -3
  17. package/skills/first-tree/engine/commands/init.ts +1 -1
  18. package/skills/first-tree/engine/commands/upgrade.ts +1 -1
  19. package/skills/first-tree/engine/commands/verify.ts +1 -1
  20. package/skills/first-tree/engine/init.ts +285 -16
  21. package/skills/first-tree/engine/repo.ts +185 -9
  22. package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
  23. package/skills/first-tree/engine/runtime/asset-loader.ts +7 -0
  24. package/skills/first-tree/engine/upgrade.ts +66 -9
  25. package/skills/first-tree/engine/validators/nodes.ts +48 -3
  26. package/skills/first-tree/engine/verify.ts +61 -3
  27. package/skills/first-tree/references/maintainer-architecture.md +1 -1
  28. package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
  29. package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
  30. package/skills/first-tree/references/onboarding.md +32 -9
  31. package/skills/first-tree/references/source-map.md +3 -3
  32. package/skills/first-tree/references/upgrade-contract.md +14 -5
  33. package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
  34. package/skills/first-tree/tests/helpers.ts +24 -4
  35. package/skills/first-tree/tests/init.test.ts +103 -6
  36. package/skills/first-tree/tests/repo.test.ts +87 -9
  37. package/skills/first-tree/tests/rules.test.ts +26 -7
  38. package/skills/first-tree/tests/skill-artifacts.test.ts +4 -0
  39. package/skills/first-tree/tests/thin-cli.test.ts +52 -7
  40. package/skills/first-tree/tests/upgrade.test.ts +19 -5
  41. package/skills/first-tree/tests/verify.test.ts +106 -7
  42. package/dist/onboarding-6Fr5Gkrk.js +0 -2
  43. 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
- const TEMPLATE_MAP: [string, string][] = [
30
- ["root-node.md.template", "NODE.md"],
31
- ["agent.md.template", "AGENT.md"],
32
- ["members-domain.md.template", "members/NODE.md"],
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 [templateName, targetPath] of TEMPLATE_MAP) {
45
- if (existsSync(join(target, targetPath))) {
46
- console.log(` Skipped ${targetPath} (already exists)`);
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(groups: RuleResult[]): string {
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("- [ ] AGENT.md exists with framework markers");
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 r = repo ?? new Repo();
106
-
107
- if (!r.isGitRepo()) {
172
+ const sourceRepo = repo ?? new Repo();
173
+ const initTarget = resolveInitTarget(sourceRepo, options);
174
+ if (initTarget.ok === false) {
108
175
  console.error(
109
- "Error: not a git repository. Initialize one first:\n git init",
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
- this.root = resolve(root ?? process.cwd());
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
- try {
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
- hasAgentMdMarkers(): boolean {
140
- const text = this.readFile("AGENT.md");
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 { FRAMEWORK_TEMPLATES_DIR } from "#skill/engine/runtime/asset-loader.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";
5
10
 
6
11
  export function evaluate(repo: Repo): RuleResult {
7
12
  const tasks: string[] = [];
8
- if (!repo.pathExists("AGENT.md")) {
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
- `AGENT.md is missing create from \`${FRAMEWORK_TEMPLATES_DIR}/agent.md.template\``,
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 (!repo.hasAgentMdMarkers()) {
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
- "AGENT.md exists but is missing framework markers — add `<!-- BEGIN CONTEXT-TREE FRAMEWORK -->` and `<!-- END CONTEXT-TREE FRAMEWORK -->` sections",
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.readFile("AGENT.md") ?? "";
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
- "Add your project-specific instructions below the framework markers in AGENT.md",
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,