first-tree 0.0.3 → 0.0.5

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 (59) hide show
  1. package/README.md +78 -27
  2. package/dist/cli.js +28 -13
  3. package/dist/{help-xEI-s9iN.js → help-5-WG9QFm.js} +1 -1
  4. package/dist/{init-DtOjj0wc.js → init-CAq0Uhq6.js} +187 -25
  5. package/dist/{installer-rcZpGLnM.js → installer-UgNasLjl.js} +20 -16
  6. package/dist/onboarding-3zYUeYQb.js +2 -0
  7. package/dist/onboarding-Dd63N-V1.js +10 -0
  8. package/dist/repo-DkR12VUv.js +369 -0
  9. package/dist/upgrade-DYzuvv1k.js +140 -0
  10. package/dist/{verify-CxN6JiV9.js → verify-C0IUSkMZ.js} +66 -6
  11. package/package.json +12 -10
  12. package/skills/first-tree/SKILL.md +18 -10
  13. package/skills/first-tree/assets/framework/VERSION +1 -1
  14. package/skills/first-tree/assets/framework/examples/claude-code/README.md +2 -2
  15. package/skills/first-tree/assets/framework/examples/claude-code/settings.json +1 -1
  16. package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +1 -1
  17. package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +0 -0
  18. package/skills/first-tree/assets/framework/helpers/run-review.ts +17 -3
  19. package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +3 -2
  20. package/skills/first-tree/assets/framework/templates/members-domain.md.template +1 -1
  21. package/skills/first-tree/assets/framework/templates/root-node.md.template +9 -6
  22. package/skills/first-tree/assets/framework/workflows/codeowners.yml +1 -1
  23. package/skills/first-tree/assets/framework/workflows/pr-review.yml +1 -1
  24. package/skills/first-tree/engine/commands/init.ts +1 -1
  25. package/skills/first-tree/engine/commands/upgrade.ts +1 -1
  26. package/skills/first-tree/engine/commands/verify.ts +1 -1
  27. package/skills/first-tree/engine/init.ts +288 -18
  28. package/skills/first-tree/engine/repo.ts +220 -11
  29. package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
  30. package/skills/first-tree/engine/rules/agent-integration.ts +3 -1
  31. package/skills/first-tree/engine/rules/framework.ts +2 -2
  32. package/skills/first-tree/engine/runtime/adapters.ts +6 -2
  33. package/skills/first-tree/engine/runtime/asset-loader.ts +143 -4
  34. package/skills/first-tree/engine/runtime/installer.ts +18 -12
  35. package/skills/first-tree/engine/upgrade.ts +99 -15
  36. package/skills/first-tree/engine/validators/nodes.ts +48 -3
  37. package/skills/first-tree/engine/verify.ts +61 -3
  38. package/skills/first-tree/references/maintainer-architecture.md +1 -1
  39. package/skills/first-tree/references/maintainer-build-and-distribution.md +3 -0
  40. package/skills/first-tree/references/maintainer-thin-cli.md +1 -1
  41. package/skills/first-tree/references/onboarding.md +57 -24
  42. package/skills/first-tree/references/source-map.md +3 -3
  43. package/skills/first-tree/references/upgrade-contract.md +62 -27
  44. package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
  45. package/skills/first-tree/scripts/quick_validate.py +0 -0
  46. package/skills/first-tree/scripts/run-local-cli.sh +0 -0
  47. package/skills/first-tree/tests/asset-loader.test.ts +23 -1
  48. package/skills/first-tree/tests/helpers.ts +51 -7
  49. package/skills/first-tree/tests/init.test.ts +113 -8
  50. package/skills/first-tree/tests/repo.test.ts +113 -9
  51. package/skills/first-tree/tests/rules.test.ts +35 -14
  52. package/skills/first-tree/tests/skill-artifacts.test.ts +10 -0
  53. package/skills/first-tree/tests/thin-cli.test.ts +52 -7
  54. package/skills/first-tree/tests/upgrade.test.ts +39 -6
  55. package/skills/first-tree/tests/verify.test.ts +109 -10
  56. package/dist/onboarding-6Fr5Gkrk.js +0 -2
  57. package/dist/onboarding-B9zPGvvG.js +0 -10
  58. package/dist/repo-BTJG8BU1.js +0 -187
  59. package/dist/upgrade-COGgI7Rj.js +0 -96
@@ -70,7 +70,7 @@ require_file "$SOURCE_DIR/assets/framework/manifest.json"
70
70
  require_file "$SOURCE_DIR/assets/framework/VERSION"
71
71
  require_file "$SOURCE_DIR/assets/framework/prompts/pr-review.md"
72
72
  require_file "$SOURCE_DIR/assets/framework/templates/root-node.md.template"
73
- require_file "$SOURCE_DIR/assets/framework/templates/agent.md.template"
73
+ require_file "$SOURCE_DIR/assets/framework/templates/agents.md.template"
74
74
  require_file "$SOURCE_DIR/assets/framework/templates/members-domain.md.template"
75
75
  require_file "$SOURCE_DIR/assets/framework/templates/member-node.md.template"
76
76
  require_file "$SOURCE_DIR/assets/framework/workflows/validate.yml"
File without changes
File without changes
@@ -2,8 +2,11 @@ import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import {
5
+ CLAUDE_INSTALLED_PROGRESS,
5
6
  FRAMEWORK_VERSION,
6
7
  INSTALLED_PROGRESS,
8
+ LEGACY_REPO_SKILL_PROGRESS,
9
+ LEGACY_REPO_SKILL_VERSION,
7
10
  LEGACY_SKILL_PROGRESS,
8
11
  LEGACY_SKILL_VERSION,
9
12
  LEGACY_PROGRESS,
@@ -18,7 +21,7 @@ import { useTmpDir } from "./helpers.js";
18
21
  describe("asset-loader", () => {
19
22
  it("prefers the installed skill layout when both layouts exist", () => {
20
23
  const tmp = useTmpDir();
21
- mkdirSync(join(tmp.path, "skills", "first-tree", "assets", "framework"), {
24
+ mkdirSync(join(tmp.path, ".agents", "skills", "first-tree", "assets", "framework"), {
22
25
  recursive: true,
23
26
  });
24
27
  mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
@@ -39,6 +42,21 @@ describe("asset-loader", () => {
39
42
  expect(detectFrameworkLayout(tmp.path)).toBe("legacy");
40
43
  });
41
44
 
45
+ it("detects the previous workspace skill path before older layouts", () => {
46
+ const tmp = useTmpDir();
47
+ mkdirSync(join(tmp.path, "skills", "first-tree", "assets", "framework"), {
48
+ recursive: true,
49
+ });
50
+ mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
51
+ writeFileSync(join(tmp.path, LEGACY_REPO_SKILL_VERSION), "0.2.0\n");
52
+ writeFileSync(join(tmp.path, LEGACY_VERSION), "0.1.0\n");
53
+
54
+ expect(detectFrameworkLayout(tmp.path)).toBe("legacy-repo-skill");
55
+ expect(
56
+ resolveFirstExistingPath(tmp.path, frameworkVersionCandidates()),
57
+ ).toBe(LEGACY_REPO_SKILL_VERSION);
58
+ });
59
+
42
60
  it("detects the previous installed skill name before the .context-tree layout", () => {
43
61
  const tmp = useTmpDir();
44
62
  mkdirSync(
@@ -59,12 +77,16 @@ describe("asset-loader", () => {
59
77
 
60
78
  it("prefers the installed progress file candidate", () => {
61
79
  const tmp = useTmpDir();
80
+ mkdirSync(join(tmp.path, ".agents", "skills", "first-tree"), { recursive: true });
81
+ mkdirSync(join(tmp.path, ".claude", "skills", "first-tree"), { recursive: true });
62
82
  mkdirSync(join(tmp.path, "skills", "first-tree"), { recursive: true });
63
83
  mkdirSync(join(tmp.path, "skills", "first-tree-cli-framework"), {
64
84
  recursive: true,
65
85
  });
66
86
  mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
67
87
  writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "new");
88
+ writeFileSync(join(tmp.path, CLAUDE_INSTALLED_PROGRESS), "claude");
89
+ writeFileSync(join(tmp.path, LEGACY_REPO_SKILL_PROGRESS), "old-repo-skill");
68
90
  writeFileSync(join(tmp.path, LEGACY_SKILL_PROGRESS), "old-skill");
69
91
  writeFileSync(join(tmp.path, LEGACY_PROGRESS), "old");
70
92
 
@@ -3,9 +3,15 @@ import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { afterEach } from "vitest";
5
5
  import {
6
+ AGENT_INSTRUCTIONS_FILE,
7
+ AGENT_INSTRUCTIONS_TEMPLATE,
8
+ CLAUDE_SKILL_ROOT,
6
9
  FRAMEWORK_VERSION,
10
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
11
+ LEGACY_REPO_SKILL_VERSION,
7
12
  LEGACY_SKILL_VERSION,
8
13
  LEGACY_VERSION,
14
+ SKILL_ROOT,
9
15
  } from "#skill/engine/runtime/asset-loader.js";
10
16
 
11
17
  interface TmpDir {
@@ -21,10 +27,34 @@ export function useTmpDir(): TmpDir {
21
27
  }
22
28
 
23
29
  export function makeFramework(root: string, version = "0.1.0"): void {
24
- mkdirSync(join(root, "skills", "first-tree", "assets", "framework"), {
25
- recursive: true,
26
- });
30
+ for (const skillRoot of [SKILL_ROOT, CLAUDE_SKILL_ROOT]) {
31
+ mkdirSync(join(root, skillRoot, "assets", "framework"), {
32
+ recursive: true,
33
+ });
34
+ writeFileSync(
35
+ join(root, skillRoot, "SKILL.md"),
36
+ "---\nname: first-tree\ndescription: installed\n---\n",
37
+ );
38
+ }
27
39
  writeFileSync(join(root, FRAMEWORK_VERSION), `${version}\n`);
40
+ writeFileSync(
41
+ join(root, CLAUDE_SKILL_ROOT, "assets", "framework", "VERSION"),
42
+ `${version}\n`,
43
+ );
44
+ }
45
+
46
+ export function makeGitRepo(root: string): void {
47
+ mkdirSync(join(root, ".git"), { recursive: true });
48
+ }
49
+
50
+ export function makeSourceRepo(root: string): void {
51
+ makeGitRepo(root);
52
+ mkdirSync(join(root, "src"), { recursive: true });
53
+ writeFileSync(
54
+ join(root, "package.json"),
55
+ JSON.stringify({ name: "example-source-repo" }, null, 2),
56
+ );
57
+ writeFileSync(join(root, "src", "index.ts"), "export const ready = true;\n");
28
58
  }
29
59
 
30
60
  export function makeLegacyFramework(root: string, version = "0.1.0"): void {
@@ -33,6 +63,17 @@ export function makeLegacyFramework(root: string, version = "0.1.0"): void {
33
63
  writeFileSync(join(root, LEGACY_VERSION), `${version}\n`);
34
64
  }
35
65
 
66
+ export function makeLegacyRepoFramework(root: string, version = "0.1.0"): void {
67
+ mkdirSync(join(root, "skills", "first-tree", "assets", "framework"), {
68
+ recursive: true,
69
+ });
70
+ writeFileSync(
71
+ join(root, "skills", "first-tree", "SKILL.md"),
72
+ "---\nname: first-tree\ndescription: legacy installed\n---\n",
73
+ );
74
+ writeFileSync(join(root, LEGACY_REPO_SKILL_VERSION), `${version}\n`);
75
+ }
76
+
36
77
  export function makeLegacyNamedFramework(
37
78
  root: string,
38
79
  version = "0.1.0",
@@ -85,7 +126,7 @@ export function makeSourceSkill(root: string, version = "0.2.0"): void {
85
126
  "assets",
86
127
  "framework",
87
128
  "templates",
88
- "agent.md.template",
129
+ AGENT_INSTRUCTIONS_TEMPLATE,
89
130
  ),
90
131
  "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nframework text\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
91
132
  );
@@ -114,12 +155,15 @@ export function makeNode(
114
155
  );
115
156
  }
116
157
 
117
- export function makeAgentMd(
158
+ export function makeAgentsMd(
118
159
  root: string,
119
- opts?: { markers?: boolean; userContent?: boolean },
160
+ opts?: { markers?: boolean; userContent?: boolean; legacyName?: boolean },
120
161
  ): void {
121
162
  const markers = opts?.markers ?? true;
122
163
  const userContent = opts?.userContent ?? false;
164
+ const fileName = opts?.legacyName
165
+ ? LEGACY_AGENT_INSTRUCTIONS_FILE
166
+ : AGENT_INSTRUCTIONS_FILE;
123
167
  const parts: string[] = [];
124
168
  if (markers) {
125
169
  parts.push(
@@ -131,7 +175,7 @@ export function makeAgentMd(
131
175
  if (userContent) {
132
176
  parts.push("\n# Project-specific\nThis is real user content.\n");
133
177
  }
134
- writeFileSync(join(root, "AGENT.md"), parts.join("\n"));
178
+ writeFileSync(join(root, fileName), parts.join("\n"));
135
179
  }
136
180
 
137
181
  export function makeMembers(root: string, count = 1): void {
@@ -1,17 +1,27 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
- import { formatTaskList, writeProgress, runInit } from "#skill/engine/init.js";
4
+ import {
5
+ formatTaskList,
6
+ parseInitArgs,
7
+ writeProgress,
8
+ runInit,
9
+ } from "#skill/engine/init.js";
5
10
  import { Repo } from "#skill/engine/repo.js";
6
11
  import {
12
+ AGENT_INSTRUCTIONS_FILE,
7
13
  FRAMEWORK_VERSION,
8
14
  INSTALLED_PROGRESS,
15
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
9
16
  LEGACY_PROGRESS,
10
17
  } from "#skill/engine/runtime/asset-loader.js";
11
18
  import {
19
+ makeGitRepo,
12
20
  useTmpDir,
21
+ makeAgentsMd,
13
22
  makeFramework,
14
23
  makeLegacyFramework,
24
+ makeSourceRepo,
15
25
  makeSourceSkill,
16
26
  } from "./helpers.js";
17
27
 
@@ -90,7 +100,7 @@ describe("writeProgress", () => {
90
100
 
91
101
  it("overwrites existing file", () => {
92
102
  const tmp = useTmpDir();
93
- mkdirSync(join(tmp.path, "skills", "first-tree"), {
103
+ mkdirSync(join(tmp.path, ".agents", "skills", "first-tree"), {
94
104
  recursive: true,
95
105
  });
96
106
  writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "old");
@@ -110,6 +120,10 @@ describe("writeProgress", () => {
110
120
 
111
121
  // --- runInit — guard logic (no network) ---
112
122
 
123
+ const fakeGitInitializer = (root: string): void => {
124
+ makeGitRepo(root);
125
+ };
126
+
113
127
  describe("runInit", () => {
114
128
  it("errors when not a git repo", () => {
115
129
  const tmp = useTmpDir();
@@ -121,26 +135,49 @@ describe("runInit", () => {
121
135
  it("installs the bundled skill and scaffolding when framework is missing", () => {
122
136
  const repoDir = useTmpDir();
123
137
  const sourceDir = useTmpDir();
124
- mkdirSync(join(repoDir.path, ".git"));
138
+ makeGitRepo(repoDir.path);
125
139
  makeSourceSkill(sourceDir.path, "0.2.0");
126
140
 
127
- const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
141
+ const ret = runInit(new Repo(repoDir.path), {
142
+ sourceRoot: sourceDir.path,
143
+ gitInitializer: fakeGitInitializer,
144
+ });
128
145
 
129
146
  expect(ret).toBe(0);
130
147
  expect(
131
- existsSync(join(repoDir.path, "skills", "first-tree", "SKILL.md")),
148
+ existsSync(join(repoDir.path, ".agents", "skills", "first-tree", "SKILL.md")),
149
+ ).toBe(true);
150
+ expect(
151
+ existsSync(join(repoDir.path, ".claude", "skills", "first-tree", "SKILL.md")),
132
152
  ).toBe(true);
133
153
  expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
134
154
  expect(existsSync(join(repoDir.path, "NODE.md"))).toBe(true);
135
- expect(existsSync(join(repoDir.path, "AGENT.md"))).toBe(true);
155
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
136
156
  expect(existsSync(join(repoDir.path, "members", "NODE.md"))).toBe(true);
137
157
  expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(true);
138
158
  });
139
159
 
160
+ it("does not scaffold AGENTS.md when legacy AGENT.md already exists", () => {
161
+ const repoDir = useTmpDir();
162
+ const sourceDir = useTmpDir();
163
+ mkdirSync(join(repoDir.path, ".git"));
164
+ makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
165
+ makeSourceSkill(sourceDir.path, "0.2.0");
166
+
167
+ const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
168
+
169
+ expect(ret).toBe(0);
170
+ expect(existsSync(join(repoDir.path, LEGACY_AGENT_INSTRUCTIONS_FILE))).toBe(true);
171
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(false);
172
+ expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
173
+ "Rename `AGENT.md` to `AGENTS.md`",
174
+ );
175
+ });
176
+
140
177
  it("skips reinstall when framework exists", () => {
141
178
  const tmp = useTmpDir();
142
179
  const sourceDir = useTmpDir();
143
- mkdirSync(join(tmp.path, ".git"));
180
+ makeGitRepo(tmp.path);
144
181
  makeFramework(tmp.path, "0.1.0");
145
182
  makeSourceSkill(sourceDir.path, "0.2.0");
146
183
 
@@ -150,4 +187,72 @@ describe("runInit", () => {
150
187
  expect(ret).toBe(0);
151
188
  expect(readFileSync(join(tmp.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.1.0");
152
189
  });
190
+
191
+ it("creates a sibling tree repo by default when invoked from a source repo", () => {
192
+ const sourceRepoDir = useTmpDir();
193
+ const sourceSkillDir = useTmpDir();
194
+ makeSourceRepo(sourceRepoDir.path);
195
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
196
+
197
+ const ret = runInit(new Repo(sourceRepoDir.path), {
198
+ sourceRoot: sourceSkillDir.path,
199
+ gitInitializer: fakeGitInitializer,
200
+ });
201
+
202
+ const treeRepo = join(
203
+ dirname(sourceRepoDir.path),
204
+ `${basename(sourceRepoDir.path)}-context`,
205
+ );
206
+
207
+ expect(ret).toBe(0);
208
+ expect(
209
+ existsSync(join(treeRepo, ".agents", "skills", "first-tree", "SKILL.md")),
210
+ ).toBe(true);
211
+ expect(
212
+ existsSync(join(treeRepo, ".claude", "skills", "first-tree", "SKILL.md")),
213
+ ).toBe(true);
214
+ expect(existsSync(join(treeRepo, "NODE.md"))).toBe(true);
215
+ expect(existsSync(join(treeRepo, AGENT_INSTRUCTIONS_FILE))).toBe(true);
216
+ expect(existsSync(join(treeRepo, "members", "NODE.md"))).toBe(true);
217
+ expect(existsSync(join(treeRepo, INSTALLED_PROGRESS))).toBe(true);
218
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(false);
219
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(false);
220
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(false);
221
+ });
222
+
223
+ it("keeps supporting in-place init with --here", () => {
224
+ const sourceRepoDir = useTmpDir();
225
+ const sourceSkillDir = useTmpDir();
226
+ makeSourceRepo(sourceRepoDir.path);
227
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
228
+
229
+ const ret = runInit(new Repo(sourceRepoDir.path), {
230
+ here: true,
231
+ sourceRoot: sourceSkillDir.path,
232
+ gitInitializer: fakeGitInitializer,
233
+ });
234
+
235
+ expect(ret).toBe(0);
236
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(true);
237
+ expect(existsSync(join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
238
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(true);
239
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(true);
240
+ });
241
+ });
242
+
243
+ describe("parseInitArgs", () => {
244
+ it("parses dedicated repo options", () => {
245
+ expect(parseInitArgs(["--tree-name", "acme-context"])).toEqual({
246
+ treeName: "acme-context",
247
+ });
248
+ expect(parseInitArgs(["--tree-path", "../acme-context"])).toEqual({
249
+ treePath: "../acme-context",
250
+ });
251
+ });
252
+
253
+ it("rejects incompatible init options", () => {
254
+ expect(parseInitArgs(["--here", "--tree-name", "acme-context"])).toEqual({
255
+ error: "Cannot combine --here with --tree-name",
256
+ });
257
+ });
153
258
  });
@@ -3,8 +3,13 @@ import { join } from "node:path";
3
3
  import { describe, expect, it } from "vitest";
4
4
  import { Repo } from "#skill/engine/repo.js";
5
5
  import {
6
+ AGENT_INSTRUCTIONS_FILE,
7
+ CLAUDE_INSTALLED_PROGRESS,
6
8
  FRAMEWORK_VERSION,
7
9
  INSTALLED_PROGRESS,
10
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
11
+ LEGACY_REPO_SKILL_PROGRESS,
12
+ LEGACY_REPO_SKILL_VERSION,
8
13
  LEGACY_SKILL_PROGRESS,
9
14
  LEGACY_SKILL_VERSION,
10
15
  LEGACY_PROGRESS,
@@ -13,8 +18,12 @@ import {
13
18
  import {
14
19
  useTmpDir,
15
20
  makeFramework,
21
+ makeGitRepo,
16
22
  makeLegacyFramework,
23
+ makeLegacyRepoFramework,
17
24
  makeLegacyNamedFramework,
25
+ makeSourceRepo,
26
+ makeSourceSkill,
18
27
  } from "./helpers.js";
19
28
 
20
29
  // --- pathExists ---
@@ -134,7 +143,14 @@ describe("anyAgentConfig", () => {
134
143
  describe("isGitRepo", () => {
135
144
  it("returns true with .git dir", () => {
136
145
  const tmp = useTmpDir();
137
- mkdirSync(join(tmp.path, ".git"));
146
+ makeGitRepo(tmp.path);
147
+ const repo = new Repo(tmp.path);
148
+ expect(repo.isGitRepo()).toBe(true);
149
+ });
150
+
151
+ it("returns true with .git file", () => {
152
+ const tmp = useTmpDir();
153
+ writeFileSync(join(tmp.path, ".git"), "gitdir: /tmp/example\n");
138
154
  const repo = new Repo(tmp.path);
139
155
  expect(repo.isGitRepo()).toBe(true);
140
156
  });
@@ -170,6 +186,13 @@ describe("hasFramework", () => {
170
186
  expect(repo.hasFramework()).toBe(true);
171
187
  });
172
188
 
189
+ it("returns true with the previous workspace skill path", () => {
190
+ const tmp = useTmpDir();
191
+ makeLegacyRepoFramework(tmp.path);
192
+ const repo = new Repo(tmp.path);
193
+ expect(repo.hasFramework()).toBe(true);
194
+ });
195
+
173
196
  it("returns false without VERSION file", () => {
174
197
  const tmp = useTmpDir();
175
198
  const repo = new Repo(tmp.path);
@@ -201,6 +224,13 @@ describe("readVersion", () => {
201
224
  expect(repo.readVersion()).toBe("0.2.5");
202
225
  });
203
226
 
227
+ it("reads the previous workspace skill version", () => {
228
+ const tmp = useTmpDir();
229
+ makeLegacyRepoFramework(tmp.path, "0.2.4");
230
+ const repo = new Repo(tmp.path);
231
+ expect(repo.readVersion()).toBe("0.2.4");
232
+ });
233
+
204
234
  it("returns null when missing", () => {
205
235
  const tmp = useTmpDir();
206
236
  const repo = new Repo(tmp.path);
@@ -233,35 +263,66 @@ describe("path preferences", () => {
233
263
  expect(repo.preferredProgressPath()).toBe(LEGACY_SKILL_PROGRESS);
234
264
  expect(repo.frameworkVersionPath()).toBe(LEGACY_SKILL_VERSION);
235
265
  });
266
+
267
+ it("switches path preferences for repos using the previous workspace skill path", () => {
268
+ const tmp = useTmpDir();
269
+ makeLegacyRepoFramework(tmp.path);
270
+ const repo = new Repo(tmp.path);
271
+ expect(repo.preferredProgressPath()).toBe(LEGACY_REPO_SKILL_PROGRESS);
272
+ expect(repo.frameworkVersionPath()).toBe(LEGACY_REPO_SKILL_VERSION);
273
+ });
236
274
  });
237
275
 
238
- // --- hasAgentMdMarkers ---
276
+ // --- agent instructions helpers ---
239
277
 
240
- describe("hasAgentMdMarkers", () => {
241
- it("returns true with markers", () => {
278
+ describe("agent instructions helpers", () => {
279
+ it("prefers AGENTS.md when both filenames exist", () => {
242
280
  const tmp = useTmpDir();
243
281
  writeFileSync(
244
- join(tmp.path, "AGENT.md"),
282
+ join(tmp.path, AGENT_INSTRUCTIONS_FILE),
245
283
  "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
246
284
  );
285
+ writeFileSync(
286
+ join(tmp.path, LEGACY_AGENT_INSTRUCTIONS_FILE),
287
+ "# Legacy instructions\n",
288
+ );
247
289
  const repo = new Repo(tmp.path);
248
- expect(repo.hasAgentMdMarkers()).toBe(true);
290
+ expect(repo.agentInstructionsPath()).toBe(AGENT_INSTRUCTIONS_FILE);
291
+ expect(repo.hasCanonicalAgentInstructionsFile()).toBe(true);
292
+ expect(repo.hasLegacyAgentInstructionsFile()).toBe(true);
293
+ expect(repo.hasDuplicateAgentInstructionsFiles()).toBe(true);
294
+ expect(repo.hasAgentInstructionsMarkers()).toBe(true);
295
+ });
296
+
297
+ it("falls back to legacy AGENT.md while migrating", () => {
298
+ const tmp = useTmpDir();
299
+ writeFileSync(
300
+ join(tmp.path, LEGACY_AGENT_INSTRUCTIONS_FILE),
301
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
302
+ );
303
+ const repo = new Repo(tmp.path);
304
+ expect(repo.agentInstructionsPath()).toBe(LEGACY_AGENT_INSTRUCTIONS_FILE);
305
+ expect(repo.hasCanonicalAgentInstructionsFile()).toBe(false);
306
+ expect(repo.hasLegacyAgentInstructionsFile()).toBe(true);
307
+ expect(repo.hasDuplicateAgentInstructionsFiles()).toBe(false);
308
+ expect(repo.hasAgentInstructionsMarkers()).toBe(true);
249
309
  });
250
310
 
251
311
  it("returns false without markers", () => {
252
312
  const tmp = useTmpDir();
253
313
  writeFileSync(
254
- join(tmp.path, "AGENT.md"),
314
+ join(tmp.path, AGENT_INSTRUCTIONS_FILE),
255
315
  "# Agent instructions\nNo markers here.\n",
256
316
  );
257
317
  const repo = new Repo(tmp.path);
258
- expect(repo.hasAgentMdMarkers()).toBe(false);
318
+ expect(repo.hasAgentInstructionsMarkers()).toBe(false);
259
319
  });
260
320
 
261
321
  it("returns false when file is missing", () => {
262
322
  const tmp = useTmpDir();
263
323
  const repo = new Repo(tmp.path);
264
- expect(repo.hasAgentMdMarkers()).toBe(false);
324
+ expect(repo.agentInstructionsPath()).toBeNull();
325
+ expect(repo.hasAgentInstructionsMarkers()).toBe(false);
265
326
  });
266
327
  });
267
328
 
@@ -360,3 +421,46 @@ describe("hasPlaceholderNode", () => {
360
421
  expect(repo.hasPlaceholderNode()).toBe(false);
361
422
  });
362
423
  });
424
+
425
+ // --- init heuristics ---
426
+
427
+ describe("init heuristics", () => {
428
+ it("treats a code repo as a likely source repo", () => {
429
+ const tmp = useTmpDir();
430
+ makeSourceRepo(tmp.path);
431
+ const repo = new Repo(tmp.path);
432
+ expect(repo.isLikelySourceRepo()).toBe(true);
433
+ expect(repo.isLikelyEmptyRepo()).toBe(false);
434
+ });
435
+
436
+ it("treats a fresh tree repo as empty enough for in-place init", () => {
437
+ const tmp = useTmpDir();
438
+ makeGitRepo(tmp.path);
439
+ writeFileSync(join(tmp.path, "README.md"), "# My Org Context\n");
440
+ const repo = new Repo(tmp.path);
441
+ expect(repo.isLikelyEmptyRepo()).toBe(true);
442
+ expect(repo.isLikelySourceRepo()).toBe(false);
443
+ });
444
+
445
+ it("recognizes a populated tree repo", () => {
446
+ const tmp = useTmpDir();
447
+ makeFramework(tmp.path);
448
+ writeFileSync(
449
+ join(tmp.path, "NODE.md"),
450
+ "---\ntitle: My Tree\nowners: [alice]\n---\n# Tree\n",
451
+ );
452
+ const repo = new Repo(tmp.path);
453
+ expect(repo.looksLikeTreeRepo()).toBe(true);
454
+ expect(repo.isLikelySourceRepo()).toBe(false);
455
+ });
456
+
457
+ it("does not mistake the framework source repo for a user tree repo", () => {
458
+ const tmp = useTmpDir();
459
+ makeSourceRepo(tmp.path);
460
+ makeSourceSkill(tmp.path, "0.2.0");
461
+ writeFileSync(join(tmp.path, "src", "cli.ts"), "export {};\n");
462
+ const repo = new Repo(tmp.path);
463
+ expect(repo.looksLikeTreeRepo()).toBe(false);
464
+ expect(repo.isLikelySourceRepo()).toBe(true);
465
+ });
466
+ });
@@ -16,7 +16,7 @@ import {
16
16
  useTmpDir,
17
17
  makeFramework,
18
18
  makeNode,
19
- makeAgentMd,
19
+ makeAgentsMd,
20
20
  makeMembers,
21
21
  } from "./helpers.js";
22
22
 
@@ -29,7 +29,7 @@ describe("framework rule", () => {
29
29
  const result = framework.evaluate(repo);
30
30
  expect(result.group).toBe("Framework");
31
31
  expect(result.tasks).toHaveLength(1);
32
- expect(result.tasks[0]).toContain("skills/first-tree/");
32
+ expect(result.tasks[0]).toContain(".agents/skills/first-tree/");
33
33
  });
34
34
 
35
35
  it("passes when framework exists", () => {
@@ -104,16 +104,35 @@ describe("rootNode rule", () => {
104
104
  // --- agent_instructions rule ---
105
105
 
106
106
  describe("agentInstructions rule", () => {
107
- it("reports missing AGENT.md", () => {
107
+ it("reports missing AGENTS.md", () => {
108
108
  const tmp = useTmpDir();
109
109
  const repo = new Repo(tmp.path);
110
110
  const result = agentInstructions.evaluate(repo);
111
- expect(result.tasks.some((t) => t.toLowerCase().includes("missing"))).toBe(true);
111
+ expect(result.tasks.some((t) => t.includes("AGENTS.md is missing"))).toBe(true);
112
+ });
113
+
114
+ it("reports legacy AGENT.md rename", () => {
115
+ const tmp = useTmpDir();
116
+ makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
117
+ const repo = new Repo(tmp.path);
118
+ const result = agentInstructions.evaluate(repo);
119
+ expect(result.tasks.some((t) => t.includes("Rename `AGENT.md` to `AGENTS.md`"))).toBe(
120
+ true,
121
+ );
122
+ });
123
+
124
+ it("reports duplicate cleanup when both filenames exist", () => {
125
+ const tmp = useTmpDir();
126
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
127
+ makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
128
+ const repo = new Repo(tmp.path);
129
+ const result = agentInstructions.evaluate(repo);
130
+ expect(result.tasks.some((t) => t.includes("delete the legacy file"))).toBe(true);
112
131
  });
113
132
 
114
133
  it("reports no markers", () => {
115
134
  const tmp = useTmpDir();
116
- makeAgentMd(tmp.path, { markers: false });
135
+ makeAgentsMd(tmp.path, { markers: false });
117
136
  const repo = new Repo(tmp.path);
118
137
  const result = agentInstructions.evaluate(repo);
119
138
  expect(result.tasks.some((t) => t.toLowerCase().includes("markers"))).toBe(true);
@@ -121,7 +140,7 @@ describe("agentInstructions rule", () => {
121
140
 
122
141
  it("reports no user content", () => {
123
142
  const tmp = useTmpDir();
124
- makeAgentMd(tmp.path, { markers: true, userContent: false });
143
+ makeAgentsMd(tmp.path, { markers: true, userContent: false });
125
144
  const repo = new Repo(tmp.path);
126
145
  const result = agentInstructions.evaluate(repo);
127
146
  expect(result.tasks.some((t) => t.toLowerCase().includes("project-specific"))).toBe(true);
@@ -129,7 +148,7 @@ describe("agentInstructions rule", () => {
129
148
 
130
149
  it("passes with markers and user content", () => {
131
150
  const tmp = useTmpDir();
132
- makeAgentMd(tmp.path, { markers: true, userContent: true });
151
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
133
152
  const repo = new Repo(tmp.path);
134
153
  const result = agentInstructions.evaluate(repo);
135
154
  expect(result.tasks).toEqual([]);
@@ -214,7 +233,9 @@ describe("ciValidation rule", () => {
214
233
  const result = ciValidation.evaluate(repo);
215
234
  expect(result.tasks).toHaveLength(4);
216
235
  expect(result.tasks[0]).toContain("validation workflow");
217
- expect(result.tasks[0]).toContain("skills/first-tree/assets/framework/workflows/validate.yml");
236
+ expect(result.tasks[0]).toContain(
237
+ ".agents/skills/first-tree/assets/framework/workflows/validate.yml",
238
+ );
218
239
  expect(result.tasks[1]).toContain("PR reviews");
219
240
  expect(result.tasks[2]).toContain("API secret");
220
241
  expect(result.tasks[3]).toContain("CODEOWNERS");
@@ -252,7 +273,7 @@ describe("ciValidation rule", () => {
252
273
  mkdirSync(wfDir, { recursive: true });
253
274
  writeFileSync(
254
275
  join(wfDir, "pr-review.yml"),
255
- "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx skills/first-tree/assets/framework/helpers/run-review.ts\n",
276
+ "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx .agents/skills/first-tree/assets/framework/helpers/run-review.ts\n",
256
277
  );
257
278
  const repo = new Repo(tmp.path);
258
279
  const result = ciValidation.evaluate(repo);
@@ -271,7 +292,7 @@ describe("ciValidation rule", () => {
271
292
  );
272
293
  writeFileSync(
273
294
  join(wfDir, "pr-review.yml"),
274
- "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx skills/first-tree/assets/framework/helpers/run-review.ts\n",
295
+ "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx .agents/skills/first-tree/assets/framework/helpers/run-review.ts\n",
275
296
  );
276
297
  const repo = new Repo(tmp.path);
277
298
  const result = ciValidation.evaluate(repo);
@@ -289,11 +310,11 @@ describe("ciValidation rule", () => {
289
310
  );
290
311
  writeFileSync(
291
312
  join(wfDir, "pr-review.yml"),
292
- "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx skills/first-tree/assets/framework/helpers/run-review.ts\n",
313
+ "name: PR Review\non: pull_request\njobs:\n review:\n steps:\n - run: npx tsx .agents/skills/first-tree/assets/framework/helpers/run-review.ts\n",
293
314
  );
294
315
  writeFileSync(
295
316
  join(wfDir, "codeowners.yml"),
296
- "name: Update CODEOWNERS\non: pull_request\njobs:\n update:\n steps:\n - run: npx tsx skills/first-tree/assets/framework/helpers/generate-codeowners.ts\n",
317
+ "name: Update CODEOWNERS\non: pull_request\njobs:\n update:\n steps:\n - run: npx tsx .agents/skills/first-tree/assets/framework/helpers/generate-codeowners.ts\n",
297
318
  );
298
319
  const repo = new Repo(tmp.path);
299
320
  const result = ciValidation.evaluate(repo);
@@ -365,9 +386,9 @@ describe("evaluateAll", () => {
365
386
  const tmp = useTmpDir();
366
387
  makeFramework(tmp.path);
367
388
  makeNode(tmp.path);
368
- makeAgentMd(tmp.path, { markers: true, userContent: true });
389
+ makeAgentsMd(tmp.path, { markers: true, userContent: true });
369
390
  makeMembers(tmp.path, 1);
370
- mkdirSync(join(tmp.path, ".claude"));
391
+ mkdirSync(join(tmp.path, ".claude"), { recursive: true });
371
392
  writeFileSync(
372
393
  join(tmp.path, ".claude", "settings.json"),
373
394
  '{"hooks": {"inject-tree-context": true}}',