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.
- package/README.md +78 -27
- package/dist/cli.js +28 -13
- package/dist/{help-xEI-s9iN.js → help-5-WG9QFm.js} +1 -1
- package/dist/{init-DtOjj0wc.js → init-CAq0Uhq6.js} +187 -25
- package/dist/{installer-rcZpGLnM.js → installer-UgNasLjl.js} +20 -16
- package/dist/onboarding-3zYUeYQb.js +2 -0
- package/dist/onboarding-Dd63N-V1.js +10 -0
- package/dist/repo-DkR12VUv.js +369 -0
- package/dist/upgrade-DYzuvv1k.js +140 -0
- package/dist/{verify-CxN6JiV9.js → verify-C0IUSkMZ.js} +66 -6
- package/package.json +12 -10
- package/skills/first-tree/SKILL.md +18 -10
- package/skills/first-tree/assets/framework/VERSION +1 -1
- package/skills/first-tree/assets/framework/examples/claude-code/README.md +2 -2
- package/skills/first-tree/assets/framework/examples/claude-code/settings.json +1 -1
- package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +1 -1
- package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +0 -0
- package/skills/first-tree/assets/framework/helpers/run-review.ts +17 -3
- package/skills/first-tree/assets/framework/templates/{agent.md.template → agents.md.template} +3 -2
- package/skills/first-tree/assets/framework/templates/members-domain.md.template +1 -1
- package/skills/first-tree/assets/framework/templates/root-node.md.template +9 -6
- package/skills/first-tree/assets/framework/workflows/codeowners.yml +1 -1
- package/skills/first-tree/assets/framework/workflows/pr-review.yml +1 -1
- 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 +288 -18
- package/skills/first-tree/engine/repo.ts +220 -11
- package/skills/first-tree/engine/rules/agent-instructions.ts +29 -7
- package/skills/first-tree/engine/rules/agent-integration.ts +3 -1
- package/skills/first-tree/engine/rules/framework.ts +2 -2
- package/skills/first-tree/engine/runtime/adapters.ts +6 -2
- package/skills/first-tree/engine/runtime/asset-loader.ts +143 -4
- package/skills/first-tree/engine/runtime/installer.ts +18 -12
- package/skills/first-tree/engine/upgrade.ts +99 -15
- 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 +57 -24
- package/skills/first-tree/references/source-map.md +3 -3
- package/skills/first-tree/references/upgrade-contract.md +62 -27
- package/skills/first-tree/scripts/check-skill-sync.sh +1 -1
- package/skills/first-tree/scripts/quick_validate.py +0 -0
- package/skills/first-tree/scripts/run-local-cli.sh +0 -0
- package/skills/first-tree/tests/asset-loader.test.ts +23 -1
- package/skills/first-tree/tests/helpers.ts +51 -7
- package/skills/first-tree/tests/init.test.ts +113 -8
- package/skills/first-tree/tests/repo.test.ts +113 -9
- package/skills/first-tree/tests/rules.test.ts +35 -14
- package/skills/first-tree/tests/skill-artifacts.test.ts +10 -0
- package/skills/first-tree/tests/thin-cli.test.ts +52 -7
- package/skills/first-tree/tests/upgrade.test.ts +39 -6
- package/skills/first-tree/tests/verify.test.ts +109 -10
- package/dist/onboarding-6Fr5Gkrk.js +0 -2
- package/dist/onboarding-B9zPGvvG.js +0 -10
- package/dist/repo-BTJG8BU1.js +0 -187
- 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/
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
138
|
+
makeGitRepo(repoDir.path);
|
|
125
139
|
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
126
140
|
|
|
127
|
-
const ret = runInit(new Repo(repoDir.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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
276
|
+
// --- agent instructions helpers ---
|
|
239
277
|
|
|
240
|
-
describe("
|
|
241
|
-
it("
|
|
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,
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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}}',
|