first-tree 0.0.5 → 0.0.7
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 +25 -7
- package/dist/cli.js +9 -6
- package/dist/{help-5-WG9QFm.js → help-BRO4mTG6.js} +1 -1
- package/dist/{init-CAq0Uhq6.js → init-BSs0ILp_.js} +49 -9
- package/dist/onboarding-BS8btkG4.js +2 -0
- package/dist/onboarding-D3hnxIie.js +10 -0
- package/dist/{repo-DkR12VUv.js → repo-0z7N9r17.js} +22 -16
- package/dist/source-integration-C2iiN4k_.js +80 -0
- package/dist/{upgrade-DYzuvv1k.js → upgrade-DvBdbph3.js} +26 -5
- package/dist/{verify-C0IUSkMZ.js → verify-DRt5mCqO.js} +7 -3
- package/package.json +2 -2
- package/skills/first-tree/SKILL.md +31 -5
- package/skills/first-tree/agents/openai.yaml +1 -1
- package/skills/first-tree/engine/init.ts +88 -5
- package/skills/first-tree/engine/repo.ts +38 -15
- package/skills/first-tree/engine/runtime/adapters.ts +0 -2
- package/skills/first-tree/engine/runtime/asset-loader.ts +6 -36
- package/skills/first-tree/engine/runtime/installer.ts +0 -2
- package/skills/first-tree/engine/runtime/source-integration.ts +80 -0
- package/skills/first-tree/engine/upgrade.ts +68 -12
- package/skills/first-tree/engine/validators/nodes.ts +2 -11
- package/skills/first-tree/engine/verify.ts +7 -0
- package/skills/first-tree/references/maintainer-architecture.md +4 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +3 -0
- package/skills/first-tree/references/onboarding.md +33 -5
- package/skills/first-tree/references/principles.md +97 -57
- package/skills/first-tree/references/source-map.md +1 -0
- package/skills/first-tree/references/source-workspace-installation.md +64 -0
- package/skills/first-tree/references/upgrade-contract.md +23 -12
- package/skills/first-tree/scripts/check-skill-sync.sh +2 -1
- package/skills/first-tree/tests/asset-loader.test.ts +0 -24
- package/skills/first-tree/tests/helpers.ts +0 -14
- package/skills/first-tree/tests/init.test.ts +67 -0
- package/skills/first-tree/tests/repo.test.ts +20 -25
- package/skills/first-tree/tests/skill-artifacts.test.ts +43 -1
- package/skills/first-tree/tests/thin-cli.test.ts +5 -0
- package/skills/first-tree/tests/upgrade.test.ts +38 -23
- package/skills/first-tree/tests/verify.test.ts +18 -0
- package/dist/installer-UgNasLjl.js +0 -51
- package/dist/onboarding-3zYUeYQb.js +0 -2
- package/dist/onboarding-Dd63N-V1.js +0 -10
|
@@ -3,6 +3,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import {
|
|
5
5
|
formatTaskList,
|
|
6
|
+
INIT_USAGE,
|
|
6
7
|
parseInitArgs,
|
|
7
8
|
writeProgress,
|
|
8
9
|
runInit,
|
|
@@ -10,11 +11,13 @@ import {
|
|
|
10
11
|
import { Repo } from "#skill/engine/repo.js";
|
|
11
12
|
import {
|
|
12
13
|
AGENT_INSTRUCTIONS_FILE,
|
|
14
|
+
CLAUDE_INSTRUCTIONS_FILE,
|
|
13
15
|
FRAMEWORK_VERSION,
|
|
14
16
|
INSTALLED_PROGRESS,
|
|
15
17
|
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
16
18
|
LEGACY_PROGRESS,
|
|
17
19
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
20
|
+
import { buildSourceIntegrationLine } from "#skill/engine/runtime/source-integration.js";
|
|
18
21
|
import {
|
|
19
22
|
makeGitRepo,
|
|
20
23
|
useTmpDir,
|
|
@@ -124,6 +127,10 @@ const fakeGitInitializer = (root: string): void => {
|
|
|
124
127
|
makeGitRepo(root);
|
|
125
128
|
};
|
|
126
129
|
|
|
130
|
+
function escapeRegExp(text: string): string {
|
|
131
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
132
|
+
}
|
|
133
|
+
|
|
127
134
|
describe("runInit", () => {
|
|
128
135
|
it("errors when not a git repo", () => {
|
|
129
136
|
const tmp = useTmpDir();
|
|
@@ -205,6 +212,18 @@ describe("runInit", () => {
|
|
|
205
212
|
);
|
|
206
213
|
|
|
207
214
|
expect(ret).toBe(0);
|
|
215
|
+
expect(
|
|
216
|
+
existsSync(join(sourceRepoDir.path, ".agents", "skills", "first-tree", "SKILL.md")),
|
|
217
|
+
).toBe(true);
|
|
218
|
+
expect(
|
|
219
|
+
existsSync(join(sourceRepoDir.path, ".claude", "skills", "first-tree", "SKILL.md")),
|
|
220
|
+
).toBe(true);
|
|
221
|
+
expect(
|
|
222
|
+
readFileSync(join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE), "utf-8"),
|
|
223
|
+
).toContain(buildSourceIntegrationLine(basename(treeRepo)));
|
|
224
|
+
expect(
|
|
225
|
+
readFileSync(join(sourceRepoDir.path, CLAUDE_INSTRUCTIONS_FILE), "utf-8"),
|
|
226
|
+
).toContain(buildSourceIntegrationLine(basename(treeRepo)));
|
|
208
227
|
expect(
|
|
209
228
|
existsSync(join(treeRepo, ".agents", "skills", "first-tree", "SKILL.md")),
|
|
210
229
|
).toBe(true);
|
|
@@ -220,6 +239,49 @@ describe("runInit", () => {
|
|
|
220
239
|
expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
221
240
|
});
|
|
222
241
|
|
|
242
|
+
it("updates existing AGENTS.md and CLAUDE.md without duplicating the source integration line", () => {
|
|
243
|
+
const sourceRepoDir = useTmpDir();
|
|
244
|
+
const sourceSkillDir = useTmpDir();
|
|
245
|
+
makeSourceRepo(sourceRepoDir.path);
|
|
246
|
+
makeSourceSkill(sourceSkillDir.path, "0.2.0");
|
|
247
|
+
writeFileSync(join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE), "# Repo Notes\n");
|
|
248
|
+
writeFileSync(join(sourceRepoDir.path, CLAUDE_INSTRUCTIONS_FILE), "# Claude Notes\n");
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
runInit(new Repo(sourceRepoDir.path), {
|
|
252
|
+
sourceRoot: sourceSkillDir.path,
|
|
253
|
+
gitInitializer: fakeGitInitializer,
|
|
254
|
+
}),
|
|
255
|
+
).toBe(0);
|
|
256
|
+
expect(
|
|
257
|
+
runInit(new Repo(sourceRepoDir.path), {
|
|
258
|
+
sourceRoot: sourceSkillDir.path,
|
|
259
|
+
gitInitializer: fakeGitInitializer,
|
|
260
|
+
}),
|
|
261
|
+
).toBe(0);
|
|
262
|
+
|
|
263
|
+
const treeRepo = join(
|
|
264
|
+
dirname(sourceRepoDir.path),
|
|
265
|
+
`${basename(sourceRepoDir.path)}-context`,
|
|
266
|
+
);
|
|
267
|
+
const expectedLine = buildSourceIntegrationLine(basename(treeRepo));
|
|
268
|
+
const agentText = readFileSync(
|
|
269
|
+
join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE),
|
|
270
|
+
"utf-8",
|
|
271
|
+
);
|
|
272
|
+
const claudeText = readFileSync(
|
|
273
|
+
join(sourceRepoDir.path, CLAUDE_INSTRUCTIONS_FILE),
|
|
274
|
+
"utf-8",
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(
|
|
278
|
+
agentText.match(new RegExp(escapeRegExp(expectedLine), "g")),
|
|
279
|
+
).toHaveLength(1);
|
|
280
|
+
expect(
|
|
281
|
+
claudeText.match(new RegExp(escapeRegExp(expectedLine), "g")),
|
|
282
|
+
).toHaveLength(1);
|
|
283
|
+
});
|
|
284
|
+
|
|
223
285
|
it("keeps supporting in-place init with --here", () => {
|
|
224
286
|
const sourceRepoDir = useTmpDir();
|
|
225
287
|
const sourceSkillDir = useTmpDir();
|
|
@@ -241,6 +303,11 @@ describe("runInit", () => {
|
|
|
241
303
|
});
|
|
242
304
|
|
|
243
305
|
describe("parseInitArgs", () => {
|
|
306
|
+
it("documents that --here is only for dedicated tree repos", () => {
|
|
307
|
+
expect(INIT_USAGE).toContain("Do not use `--here` inside a source/workspace repo");
|
|
308
|
+
expect(INIT_USAGE).toContain("already in the dedicated tree repo");
|
|
309
|
+
});
|
|
310
|
+
|
|
244
311
|
it("parses dedicated repo options", () => {
|
|
245
312
|
expect(parseInitArgs(["--tree-name", "acme-context"])).toEqual({
|
|
246
313
|
treeName: "acme-context",
|
|
@@ -5,15 +5,15 @@ import { Repo } from "#skill/engine/repo.js";
|
|
|
5
5
|
import {
|
|
6
6
|
AGENT_INSTRUCTIONS_FILE,
|
|
7
7
|
CLAUDE_INSTALLED_PROGRESS,
|
|
8
|
+
CLAUDE_INSTRUCTIONS_FILE,
|
|
8
9
|
FRAMEWORK_VERSION,
|
|
9
10
|
INSTALLED_PROGRESS,
|
|
10
11
|
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
11
12
|
LEGACY_REPO_SKILL_PROGRESS,
|
|
12
13
|
LEGACY_REPO_SKILL_VERSION,
|
|
13
|
-
LEGACY_SKILL_PROGRESS,
|
|
14
|
-
LEGACY_SKILL_VERSION,
|
|
15
14
|
LEGACY_PROGRESS,
|
|
16
15
|
LEGACY_VERSION,
|
|
16
|
+
SOURCE_INTEGRATION_MARKER,
|
|
17
17
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
18
18
|
import {
|
|
19
19
|
useTmpDir,
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
makeGitRepo,
|
|
22
22
|
makeLegacyFramework,
|
|
23
23
|
makeLegacyRepoFramework,
|
|
24
|
-
makeLegacyNamedFramework,
|
|
25
24
|
makeSourceRepo,
|
|
26
25
|
makeSourceSkill,
|
|
27
26
|
} from "./helpers.js";
|
|
@@ -179,13 +178,6 @@ describe("hasFramework", () => {
|
|
|
179
178
|
expect(repo.hasFramework()).toBe(true);
|
|
180
179
|
});
|
|
181
180
|
|
|
182
|
-
it("returns true with the previous installed skill name", () => {
|
|
183
|
-
const tmp = useTmpDir();
|
|
184
|
-
makeLegacyNamedFramework(tmp.path);
|
|
185
|
-
const repo = new Repo(tmp.path);
|
|
186
|
-
expect(repo.hasFramework()).toBe(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
181
|
it("returns true with the previous workspace skill path", () => {
|
|
190
182
|
const tmp = useTmpDir();
|
|
191
183
|
makeLegacyRepoFramework(tmp.path);
|
|
@@ -217,13 +209,6 @@ describe("readVersion", () => {
|
|
|
217
209
|
expect(repo.readVersion()).toBe("0.3.0");
|
|
218
210
|
});
|
|
219
211
|
|
|
220
|
-
it("reads the previous installed skill version", () => {
|
|
221
|
-
const tmp = useTmpDir();
|
|
222
|
-
makeLegacyNamedFramework(tmp.path, "0.2.5");
|
|
223
|
-
const repo = new Repo(tmp.path);
|
|
224
|
-
expect(repo.readVersion()).toBe("0.2.5");
|
|
225
|
-
});
|
|
226
|
-
|
|
227
212
|
it("reads the previous workspace skill version", () => {
|
|
228
213
|
const tmp = useTmpDir();
|
|
229
214
|
makeLegacyRepoFramework(tmp.path, "0.2.4");
|
|
@@ -256,14 +241,6 @@ describe("path preferences", () => {
|
|
|
256
241
|
expect(repo.frameworkVersionPath()).toBe(LEGACY_VERSION);
|
|
257
242
|
});
|
|
258
243
|
|
|
259
|
-
it("switches path preferences for repos using the previous skill name", () => {
|
|
260
|
-
const tmp = useTmpDir();
|
|
261
|
-
makeLegacyNamedFramework(tmp.path);
|
|
262
|
-
const repo = new Repo(tmp.path);
|
|
263
|
-
expect(repo.preferredProgressPath()).toBe(LEGACY_SKILL_PROGRESS);
|
|
264
|
-
expect(repo.frameworkVersionPath()).toBe(LEGACY_SKILL_VERSION);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
244
|
it("switches path preferences for repos using the previous workspace skill path", () => {
|
|
268
245
|
const tmp = useTmpDir();
|
|
269
246
|
makeLegacyRepoFramework(tmp.path);
|
|
@@ -454,6 +431,24 @@ describe("init heuristics", () => {
|
|
|
454
431
|
expect(repo.isLikelySourceRepo()).toBe(false);
|
|
455
432
|
});
|
|
456
433
|
|
|
434
|
+
it("treats a source repo with installed skill and integration markers as a source repo", () => {
|
|
435
|
+
const tmp = useTmpDir();
|
|
436
|
+
makeSourceRepo(tmp.path);
|
|
437
|
+
makeFramework(tmp.path);
|
|
438
|
+
writeFileSync(
|
|
439
|
+
join(tmp.path, AGENT_INSTRUCTIONS_FILE),
|
|
440
|
+
`${SOURCE_INTEGRATION_MARKER} Use the installed \`first-tree\` skill here.\n`,
|
|
441
|
+
);
|
|
442
|
+
writeFileSync(
|
|
443
|
+
join(tmp.path, CLAUDE_INSTRUCTIONS_FILE),
|
|
444
|
+
`${SOURCE_INTEGRATION_MARKER} Use the installed \`first-tree\` skill here.\n`,
|
|
445
|
+
);
|
|
446
|
+
const repo = new Repo(tmp.path);
|
|
447
|
+
expect(repo.hasSourceWorkspaceIntegration()).toBe(true);
|
|
448
|
+
expect(repo.looksLikeTreeRepo()).toBe(false);
|
|
449
|
+
expect(repo.isLikelySourceRepo()).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
|
|
457
452
|
it("does not mistake the framework source repo for a user tree repo", () => {
|
|
458
453
|
const tmp = useTmpDir();
|
|
459
454
|
makeSourceRepo(tmp.path);
|
|
@@ -20,6 +20,17 @@ describe("skill artifacts", () => {
|
|
|
20
20
|
it("keeps only the canonical skill in the source repo", () => {
|
|
21
21
|
expect(existsSync(join(ROOT, "skills", "first-tree", "SKILL.md"))).toBe(true);
|
|
22
22
|
expect(existsSync(join(ROOT, "skills", "first-tree", "references", "onboarding.md"))).toBe(true);
|
|
23
|
+
expect(
|
|
24
|
+
existsSync(
|
|
25
|
+
join(
|
|
26
|
+
ROOT,
|
|
27
|
+
"skills",
|
|
28
|
+
"first-tree",
|
|
29
|
+
"references",
|
|
30
|
+
"source-workspace-installation.md",
|
|
31
|
+
),
|
|
32
|
+
),
|
|
33
|
+
).toBe(true);
|
|
23
34
|
expect(existsSync(join(ROOT, "skills", "first-tree", "assets", "framework", "manifest.json"))).toBe(true);
|
|
24
35
|
expect(existsSync(join(ROOT, "skills", "first-tree", "engine", "init.ts"))).toBe(true);
|
|
25
36
|
expect(existsSync(join(ROOT, "AGENTS.md"))).toBe(true);
|
|
@@ -43,6 +54,18 @@ describe("skill artifacts", () => {
|
|
|
43
54
|
),
|
|
44
55
|
),
|
|
45
56
|
).toBe(true);
|
|
57
|
+
expect(
|
|
58
|
+
existsSync(
|
|
59
|
+
join(
|
|
60
|
+
ROOT,
|
|
61
|
+
"skills",
|
|
62
|
+
"first-tree",
|
|
63
|
+
"engine",
|
|
64
|
+
"runtime",
|
|
65
|
+
"source-integration.ts",
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
).toBe(true);
|
|
46
69
|
expect(
|
|
47
70
|
existsSync(
|
|
48
71
|
join(
|
|
@@ -116,7 +139,6 @@ describe("skill artifacts", () => {
|
|
|
116
139
|
expect(isTrackedInGit(".claude")).toBe(false);
|
|
117
140
|
expect(isTrackedInGit(".context-tree")).toBe(false);
|
|
118
141
|
expect(existsSync(join(ROOT, "AGENT.md"))).toBe(false);
|
|
119
|
-
expect(isTrackedInGit("skills/first-tree-cli-framework")).toBe(false);
|
|
120
142
|
expect(isTrackedInGit("docs")).toBe(false);
|
|
121
143
|
expect(isTrackedInGit("tests")).toBe(false);
|
|
122
144
|
expect(existsSync(join(ROOT, "evals"))).toBe(true);
|
|
@@ -200,13 +222,17 @@ describe("skill artifacts", () => {
|
|
|
200
222
|
expect(read("README.md")).toContain("Package Name vs Command");
|
|
201
223
|
expect(read("README.md")).toContain("Canonical Documentation");
|
|
202
224
|
expect(read("README.md")).toContain("references/source-map.md");
|
|
225
|
+
expect(read("README.md")).toContain("source-workspace-installation.md");
|
|
203
226
|
expect(read("README.md")).toContain("skills/first-tree/");
|
|
204
227
|
expect(read("README.md")).toContain(".agents/skills/first-tree/");
|
|
205
228
|
expect(read("README.md")).toContain(".claude/skills/first-tree/");
|
|
206
229
|
expect(read("README.md")).toContain("bundled canonical");
|
|
207
230
|
expect(read("README.md")).toContain("dedicated tree repo");
|
|
231
|
+
expect(read("README.md")).toContain("FIRST-TREE-SOURCE-INTEGRATION:");
|
|
208
232
|
expect(read("README.md")).toContain("`first-tree` skill");
|
|
233
|
+
expect(read("README.md")).toContain("Only use `--here` after you have already switched into the dedicated tree repo.");
|
|
209
234
|
expect(read("AGENTS.md")).toContain("references/source-map.md");
|
|
235
|
+
expect(read("AGENTS.md")).toContain("source-workspace-installation.md");
|
|
210
236
|
expect(read("AGENTS.md")).toContain("bundled skill path");
|
|
211
237
|
expect(read("AGENTS.md")).not.toContain("### Running evals");
|
|
212
238
|
expect(read("AGENTS.md")).not.toContain("EVALS_TREE_REPO");
|
|
@@ -222,6 +248,10 @@ describe("skill artifacts", () => {
|
|
|
222
248
|
expect(onboarding).toContain("npx first-tree@latest upgrade");
|
|
223
249
|
expect(onboarding).toContain(".agents/skills/first-tree/");
|
|
224
250
|
expect(onboarding).toContain(".claude/skills/first-tree/");
|
|
251
|
+
expect(onboarding).toContain("FIRST-TREE-SOURCE-INTEGRATION:");
|
|
252
|
+
expect(onboarding).toContain("source/workspace repo");
|
|
253
|
+
expect(onboarding).toContain("git submodule");
|
|
254
|
+
expect(onboarding).toContain("Only use `--here` after you have already switched into the dedicated tree repo.");
|
|
225
255
|
expect(onboarding).not.toContain("This clones the framework into `.context-tree/`");
|
|
226
256
|
expect(onboarding).not.toContain("from upstream");
|
|
227
257
|
|
|
@@ -234,11 +264,15 @@ describe("skill artifacts", () => {
|
|
|
234
264
|
expect(skillMd).toContain("so it is not confused with the `first-tree`");
|
|
235
265
|
expect(skillMd).toContain(".agents/skills/first-tree/");
|
|
236
266
|
expect(skillMd).toContain(".claude/skills/first-tree/");
|
|
267
|
+
expect(skillMd).toContain("source-workspace-installation.md");
|
|
268
|
+
expect(skillMd).toContain("FIRST-TREE-SOURCE-INTEGRATION:");
|
|
269
|
+
expect(skillMd).toContain("Never run `context-tree init --here` in a source/workspace repo");
|
|
237
270
|
expect(skillMd).not.toContain("canonical eval harness");
|
|
238
271
|
|
|
239
272
|
const sourceMap = read("skills/first-tree/references/source-map.md");
|
|
240
273
|
expect(sourceMap).not.toContain("repo-snapshot");
|
|
241
274
|
expect(sourceMap).not.toContain("sync-skill-artifacts.sh");
|
|
275
|
+
expect(sourceMap).toContain("source-workspace-installation.md");
|
|
242
276
|
expect(sourceMap).toContain("maintainer-architecture.md");
|
|
243
277
|
expect(sourceMap).toContain("maintainer-thin-cli.md");
|
|
244
278
|
expect(sourceMap).toContain("maintainer-build-and-distribution.md");
|
|
@@ -252,6 +286,14 @@ describe("skill artifacts", () => {
|
|
|
252
286
|
expect(sourceMap).not.toContain("vitest.eval.config.ts");
|
|
253
287
|
expect(sourceMap).toContain(".github/workflows/ci.yml");
|
|
254
288
|
|
|
289
|
+
const sourceWorkspaceInstall = read(
|
|
290
|
+
"skills/first-tree/references/source-workspace-installation.md",
|
|
291
|
+
);
|
|
292
|
+
expect(sourceWorkspaceInstall).toContain("FIRST-TREE-SOURCE-INTEGRATION:");
|
|
293
|
+
expect(sourceWorkspaceInstall).toContain("git submodule");
|
|
294
|
+
expect(sourceWorkspaceInstall).toContain("Do not run `context-tree verify`");
|
|
295
|
+
expect(sourceWorkspaceInstall).toContain("Do not run `context-tree init --here` in the source/workspace repo");
|
|
296
|
+
|
|
255
297
|
const maintainerArchitecture = read(
|
|
256
298
|
"skills/first-tree/references/maintainer-architecture.md",
|
|
257
299
|
);
|
|
@@ -39,6 +39,11 @@ afterEach(() => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
describe("thin CLI shell", () => {
|
|
42
|
+
it("documents the dedicated-repo meaning of --here", () => {
|
|
43
|
+
expect(USAGE).toContain("git init && context-tree init --here");
|
|
44
|
+
expect(USAGE).toContain("`--here` is for when the current repo is already the dedicated tree repo.");
|
|
45
|
+
});
|
|
46
|
+
|
|
42
47
|
it("treats a symlinked npm bin path as direct execution", () => {
|
|
43
48
|
const dir = makeTempDir();
|
|
44
49
|
const target = join(dir, "cli.js");
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { Repo } from "#skill/engine/repo.js";
|
|
5
5
|
import { runUpgrade } from "#skill/engine/upgrade.js";
|
|
6
6
|
import {
|
|
7
7
|
AGENT_INSTRUCTIONS_FILE,
|
|
8
|
+
CLAUDE_INSTRUCTIONS_FILE,
|
|
8
9
|
FRAMEWORK_VERSION,
|
|
9
10
|
INSTALLED_PROGRESS,
|
|
10
11
|
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
12
|
+
SOURCE_INTEGRATION_MARKER,
|
|
11
13
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
14
|
+
import { buildSourceIntegrationLine } from "#skill/engine/runtime/source-integration.js";
|
|
12
15
|
import {
|
|
13
16
|
makeAgentsMd,
|
|
14
17
|
makeFramework,
|
|
15
18
|
makeSourceRepo,
|
|
16
19
|
makeLegacyFramework,
|
|
17
20
|
makeLegacyRepoFramework,
|
|
18
|
-
makeLegacyNamedFramework,
|
|
19
21
|
makeSourceSkill,
|
|
20
22
|
useTmpDir,
|
|
21
23
|
} from "./helpers.js";
|
|
@@ -60,26 +62,6 @@ describe("runUpgrade", () => {
|
|
|
60
62
|
expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
61
63
|
});
|
|
62
64
|
|
|
63
|
-
it("migrates repos that still use the previous installed skill name", () => {
|
|
64
|
-
const repoDir = useTmpDir();
|
|
65
|
-
const sourceDir = useTmpDir();
|
|
66
|
-
makeLegacyNamedFramework(repoDir.path, "0.2.0");
|
|
67
|
-
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
68
|
-
|
|
69
|
-
const result = runUpgrade(new Repo(repoDir.path), {
|
|
70
|
-
sourceRoot: sourceDir.path,
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
expect(result).toBe(0);
|
|
74
|
-
expect(existsSync(join(repoDir.path, "skills", "first-tree-cli-framework"))).toBe(
|
|
75
|
-
false,
|
|
76
|
-
);
|
|
77
|
-
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
78
|
-
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
79
|
-
"skills/first-tree-cli-framework/",
|
|
80
|
-
);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
65
|
it("migrates repos that still use the previous workspace skill path", () => {
|
|
84
66
|
const repoDir = useTmpDir();
|
|
85
67
|
const sourceDir = useTmpDir();
|
|
@@ -119,4 +101,37 @@ describe("runUpgrade", () => {
|
|
|
119
101
|
const result = runUpgrade(new Repo(repoDir.path));
|
|
120
102
|
expect(result).toBe(1);
|
|
121
103
|
});
|
|
104
|
+
|
|
105
|
+
it("refreshes source/workspace integration without writing tree progress", () => {
|
|
106
|
+
const repoDir = useTmpDir();
|
|
107
|
+
const sourceDir = useTmpDir();
|
|
108
|
+
makeSourceRepo(repoDir.path);
|
|
109
|
+
makeFramework(repoDir.path, "0.1.0");
|
|
110
|
+
writeFileSync(
|
|
111
|
+
join(repoDir.path, AGENT_INSTRUCTIONS_FILE),
|
|
112
|
+
`${SOURCE_INTEGRATION_MARKER} old text\n`,
|
|
113
|
+
);
|
|
114
|
+
writeFileSync(
|
|
115
|
+
join(repoDir.path, CLAUDE_INSTRUCTIONS_FILE),
|
|
116
|
+
`${SOURCE_INTEGRATION_MARKER} old text\n`,
|
|
117
|
+
);
|
|
118
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
119
|
+
|
|
120
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
121
|
+
sourceRoot: sourceDir.path,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const expectedLine = buildSourceIntegrationLine(
|
|
125
|
+
`${basename(repoDir.path)}-context`,
|
|
126
|
+
);
|
|
127
|
+
expect(result).toBe(0);
|
|
128
|
+
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
129
|
+
expect(readFileSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE), "utf-8")).toContain(
|
|
130
|
+
expectedLine,
|
|
131
|
+
);
|
|
132
|
+
expect(readFileSync(join(repoDir.path, CLAUDE_INSTRUCTIONS_FILE), "utf-8")).toContain(
|
|
133
|
+
expectedLine,
|
|
134
|
+
);
|
|
135
|
+
expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
136
|
+
});
|
|
122
137
|
});
|
|
@@ -6,8 +6,10 @@ import { check, checkProgress, runVerify } from "#skill/engine/verify.js";
|
|
|
6
6
|
import { Repo } from "#skill/engine/repo.js";
|
|
7
7
|
import {
|
|
8
8
|
AGENT_INSTRUCTIONS_FILE,
|
|
9
|
+
CLAUDE_INSTRUCTIONS_FILE,
|
|
9
10
|
INSTALLED_PROGRESS,
|
|
10
11
|
LEGACY_PROGRESS,
|
|
12
|
+
SOURCE_INTEGRATION_MARKER,
|
|
11
13
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
12
14
|
import {
|
|
13
15
|
useTmpDir,
|
|
@@ -238,4 +240,20 @@ describe("runVerify failing", () => {
|
|
|
238
240
|
const ret = runVerify(repo, passValidator);
|
|
239
241
|
expect(ret).toBe(1);
|
|
240
242
|
});
|
|
243
|
+
|
|
244
|
+
it("refuses to verify a source/workspace repo that only has local first-tree integration", () => {
|
|
245
|
+
const tmp = useTmpDir();
|
|
246
|
+
makeSourceRepo(tmp.path);
|
|
247
|
+
makeFramework(tmp.path);
|
|
248
|
+
writeFileSync(
|
|
249
|
+
join(tmp.path, AGENT_INSTRUCTIONS_FILE),
|
|
250
|
+
`${SOURCE_INTEGRATION_MARKER} Use the installed \`first-tree\` skill here.\n`,
|
|
251
|
+
);
|
|
252
|
+
writeFileSync(
|
|
253
|
+
join(tmp.path, CLAUDE_INSTRUCTIONS_FILE),
|
|
254
|
+
`${SOURCE_INTEGRATION_MARKER} Use the installed \`first-tree\` skill here.\n`,
|
|
255
|
+
);
|
|
256
|
+
const repo = new Repo(tmp.path);
|
|
257
|
+
expect(runVerify(repo, passValidator)).toBe(1);
|
|
258
|
+
});
|
|
241
259
|
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { S as LEGACY_SKILL_ROOT, a as BUNDLED_SKILL_ROOT, m as INSTALLED_SKILL_ROOTS, y as LEGACY_REPO_SKILL_ROOT } from "./repo-DkR12VUv.js";
|
|
2
|
-
import { copyFileSync, cpSync, existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
//#region skills/first-tree/engine/runtime/installer.ts
|
|
6
|
-
function resolveBundledPackageRoot(startUrl = import.meta.url) {
|
|
7
|
-
let dir = dirname(fileURLToPath(startUrl));
|
|
8
|
-
while (true) {
|
|
9
|
-
if (existsSync(join(dir, "package.json")) && existsSync(join(dir, BUNDLED_SKILL_ROOT, "SKILL.md"))) return dir;
|
|
10
|
-
const parent = dirname(dir);
|
|
11
|
-
if (parent === dir) break;
|
|
12
|
-
dir = parent;
|
|
13
|
-
}
|
|
14
|
-
throw new Error("Could not locate the bundled `first-tree` package root. Reinstall the package and try again.");
|
|
15
|
-
}
|
|
16
|
-
function resolveCanonicalSkillRoot(sourceRoot) {
|
|
17
|
-
const directSkillRoot = sourceRoot;
|
|
18
|
-
if (existsSync(join(directSkillRoot, "SKILL.md")) && existsSync(join(directSkillRoot, "assets", "framework", "VERSION"))) return directSkillRoot;
|
|
19
|
-
const nestedSkillRoot = join(sourceRoot, BUNDLED_SKILL_ROOT);
|
|
20
|
-
if (existsSync(join(nestedSkillRoot, "SKILL.md")) && existsSync(join(nestedSkillRoot, "assets", "framework", "VERSION"))) return nestedSkillRoot;
|
|
21
|
-
throw new Error(`Canonical skill not found under ${sourceRoot}. Reinstall the \`first-tree\` package and try again.`);
|
|
22
|
-
}
|
|
23
|
-
function copyCanonicalSkill(sourceRoot, targetRoot) {
|
|
24
|
-
const src = resolveCanonicalSkillRoot(sourceRoot);
|
|
25
|
-
for (const relPath of [
|
|
26
|
-
...INSTALLED_SKILL_ROOTS,
|
|
27
|
-
LEGACY_REPO_SKILL_ROOT,
|
|
28
|
-
LEGACY_SKILL_ROOT
|
|
29
|
-
]) {
|
|
30
|
-
const fullPath = join(targetRoot, relPath);
|
|
31
|
-
if (existsSync(fullPath)) rmSync(fullPath, {
|
|
32
|
-
recursive: true,
|
|
33
|
-
force: true
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
for (const relPath of INSTALLED_SKILL_ROOTS) {
|
|
37
|
-
const dst = join(targetRoot, relPath);
|
|
38
|
-
mkdirSync(dirname(dst), { recursive: true });
|
|
39
|
-
cpSync(src, dst, { recursive: true });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function renderTemplateFile(frameworkRoot, templateName, targetRoot, targetPath) {
|
|
43
|
-
const src = join(frameworkRoot, "templates", templateName);
|
|
44
|
-
const dst = join(targetRoot, targetPath);
|
|
45
|
-
if (existsSync(dst) || !existsSync(src)) return false;
|
|
46
|
-
mkdirSync(dirname(dst), { recursive: true });
|
|
47
|
-
copyFileSync(src, dst);
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
//#endregion
|
|
51
|
-
export { resolveCanonicalSkillRoot as i, renderTemplateFile as n, resolveBundledPackageRoot as r, copyCanonicalSkill as t };
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
//#region skills/first-tree/references/onboarding.md
|
|
2
|
-
var onboarding_default = "# Context Tree Onboarding\n\nYou are setting up a **Context Tree** — the living source of truth for an organization. This document tells you what it is and how to bootstrap one.\n\n---\n\n## What Is a Context Tree\n\nA Context Tree is a Git repository where every directory is a **domain** and every file is a **node**. Each node captures decisions, designs, and cross-domain relationships — the knowledge that would otherwise scatter across PRs, documents, and people's heads.\n\nKey properties:\n\n- **Nodes are markdown files.** Each directory has a `NODE.md` that describes the domain. Leaf `.md` files capture specific decisions or designs.\n- **Every node has an owner.** Declared in YAML frontmatter. Owners approve changes to their nodes.\n- **Organized by concern, not by repo or team.** An agent working on \"add SSO\" finds all auth context in one place — not split across 4 repos.\n- **The tree is never a snapshot — it's the current state.** When decisions change, the tree updates. Stale nodes are bugs.\n\n### Frontmatter Format\n\nEvery node has frontmatter:\n\n```yaml\n---\ntitle: \"Auth Architecture\"\nowners: [alice, bob]\nsoft_links: [/infrastructure/deployments]\n---\n```\n\n- `owners` — who can approve changes. `owners: []` inherits from parent. `owners: [*]` means anyone.\n- `soft_links` — cross-references to related nodes in other domains.\n\n### What Belongs in the Tree\n\nInformation an agent needs to **decide** on an approach — not to execute it.\n\n**Yes:** \"Auth spans 4 repos: backend issues JWTs, frontend uses Better Auth, extension uses OAuth popup, desktop uses localhost callback.\"\n\n**No:** The function signature of `auth_service.verify()` — that's in the code.\n\n---\n\n## Four Principles\n\n1. **Source of truth for decisions, not execution.** The tree captures the *what* and *why*. Execution details stay in source systems.\n2. **Agents are first-class participants.** The tree is designed for agents to navigate and update.\n3. **Transparency by default.** Reading is open to all. Writing requires owner approval.\n4. **Git-native.** Nodes are files, domains are directories. History, ownership, and review follow Git conventions.\n\n---\n\n## How to Set Up a Context Tree\n\n### Prerequisites\n\n- A source/workspace Git repository, or an already-created dedicated tree repo\n- Node.js 18+\n- The npm package is `first-tree`, the installed CLI command is\n `context-tree`.\n- `context-tree init` installs the framework skill into\n `.agents/skills/first-tree/` and `.claude/skills/first-tree/`.\n- Use `npx first-tree init` for one-off runs, or `npm install -g first-tree`\n to add the `context-tree` command to your PATH\n\n### Step 1: Initialize\n\nRecommended workflow: run `context-tree init` from your source or workspace repo.\nThe CLI will create a sibling dedicated tree repo named `<repo>-context` by\ndefault and install the framework there.\n\n```bash\ncd my-org\ncontext-tree init\ncd ../my-org-context\n```\n\nIf you already created a dedicated tree repo manually, initialize it in place:\n\n```bash\nmkdir my-org-context && cd my-org-context\ngit init\ncontext-tree init --here\n```\n\nEither way, the framework installs into `.agents/skills/first-tree/` and\n`.claude/skills/first-tree/`, renders scaffolding (`NODE.md`, `AGENTS.md`,\n`members/NODE.md`), and generates a task list in\n`.agents/skills/first-tree/progress.md`.\n\nPublishing tip: keep the tree repo in the same GitHub organization as the\nsource repo unless you have a reason not to.\n\n### Step 2: Work Through the Task List\n\nRead `.agents/skills/first-tree/progress.md`. It contains a checklist tailored\nto the current state of the repo. Complete each task:\n\n- Fill in `NODE.md` with your organization name, owners, and domains\n- Add project-specific instructions to `AGENTS.md` below the framework markers\n- Create member nodes under `members/`\n- Optionally configure agent integration (for Claude Code, the installed hook\n assets live under `.claude/skills/first-tree/`)\n- Copy validation workflows from\n `.agents/skills/first-tree/assets/framework/workflows/` to\n `.github/workflows/`\n\nAs you complete each task, check it off in\n`.agents/skills/first-tree/progress.md` by changing `- [ ]` to `- [x]`.\n\n### Step 3: Verify\n\n```bash\ncontext-tree verify\n```\n\nOr, from your source/workspace repo:\n\n```bash\ncontext-tree verify --tree-path ../my-org-context\n```\n\nThis fails if any items in `.agents/skills/first-tree/progress.md` remain\nunchecked, and runs deterministic checks (valid frontmatter, node structure,\nmember nodes exist).\n\n### Step 4: Design Your Domains\n\nCreate top-level directories for your organization's primary concerns. Each needs a `NODE.md`:\n\n```\nmy-org-tree/\n NODE.md # root — lists all domains\n engineering/\n NODE.md # decisions about architecture, infra, tooling\n product/\n NODE.md # strategy, roadmap, user research\n marketing/\n NODE.md # positioning, campaigns\n members/\n NODE.md # team members and agents\n alice/\n NODE.md # individual member node\n```\n\n### Step 5: Populate from Existing Work\n\nFor each domain, extract knowledge from existing repos, docs, and systems:\n\n- Decisions and their rationale\n- Cross-domain relationships and dependencies\n- Constraints that aren't obvious from the code\n\nThe tree doesn't duplicate source code — it captures what connects things and why they were built that way.\n\n---\n\n## CLI Reference\n\n| Command | Description |\n|---------|-------------|\n| `context-tree init` | Create or refresh a dedicated tree repo. By default, running in a source/workspace repo creates a sibling `<repo>-context`; use `--here` to initialize the current repo in place. |\n| `context-tree verify` | Check the installed progress file for unchecked items + run deterministic validation. Use `--tree-path` when invoking from another working directory. |\n| `context-tree upgrade` | Refresh the installed framework skill from the currently running `first-tree` npm package and generate follow-up tasks. Use `--tree-path` when invoking from another working directory. |\n| `context-tree help onboarding` | Print this onboarding guide. |\n\n---\n\n## Upgrading the Framework\n\nWhen the framework updates:\n\n```bash\ncontext-tree upgrade\n```\n\n`context-tree upgrade` refreshes `.agents/skills/first-tree/` and\n`.claude/skills/first-tree/` from the skill bundled with the currently running\n`first-tree` npm package, preserves your tree content, and generates follow-up\ntasks in `.agents/skills/first-tree/progress.md`.\n\nIf your repo still uses the older `skills/first-tree/`,\n`skills/first-tree-cli-framework/`, or `.context-tree/` layouts,\n`context-tree upgrade` will migrate it to the current installed layout first.\n\nTo pick up a newer framework release, first run a newer package version, for\nexample `npx first-tree@latest upgrade`, or update your global `first-tree`\ninstall before running `context-tree upgrade`.\n\n---\n\n## Further Reading\n\n- `.agents/skills/first-tree/references/principles.md` — Core principles with detailed examples\n- `.agents/skills/first-tree/references/ownership-and-naming.md` — How nodes are named and owned\n- `AGENTS.md` in your tree — The before/during/after workflow for every task\n";
|
|
3
|
-
//#endregion
|
|
4
|
-
//#region skills/first-tree/engine/onboarding.ts
|
|
5
|
-
function runOnboarding(output = console.log) {
|
|
6
|
-
output(onboarding_default);
|
|
7
|
-
return 0;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
10
|
-
export { onboarding_default as n, runOnboarding as t };
|