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
|
@@ -22,6 +22,7 @@ describe("skill artifacts", () => {
|
|
|
22
22
|
expect(existsSync(join(ROOT, "skills", "first-tree", "references", "onboarding.md"))).toBe(true);
|
|
23
23
|
expect(existsSync(join(ROOT, "skills", "first-tree", "assets", "framework", "manifest.json"))).toBe(true);
|
|
24
24
|
expect(existsSync(join(ROOT, "skills", "first-tree", "engine", "init.ts"))).toBe(true);
|
|
25
|
+
expect(existsSync(join(ROOT, "AGENTS.md"))).toBe(true);
|
|
25
26
|
expect(existsSync(join(ROOT, "skills", "first-tree", "tests", "init.test.ts"))).toBe(
|
|
26
27
|
true,
|
|
27
28
|
);
|
|
@@ -114,6 +115,7 @@ describe("skill artifacts", () => {
|
|
|
114
115
|
expect(isTrackedInGit(".agents")).toBe(false);
|
|
115
116
|
expect(isTrackedInGit(".claude")).toBe(false);
|
|
116
117
|
expect(isTrackedInGit(".context-tree")).toBe(false);
|
|
118
|
+
expect(existsSync(join(ROOT, "AGENT.md"))).toBe(false);
|
|
117
119
|
expect(isTrackedInGit("skills/first-tree-cli-framework")).toBe(false);
|
|
118
120
|
expect(isTrackedInGit("docs")).toBe(false);
|
|
119
121
|
expect(isTrackedInGit("tests")).toBe(false);
|
|
@@ -199,7 +201,10 @@ describe("skill artifacts", () => {
|
|
|
199
201
|
expect(read("README.md")).toContain("Canonical Documentation");
|
|
200
202
|
expect(read("README.md")).toContain("references/source-map.md");
|
|
201
203
|
expect(read("README.md")).toContain("skills/first-tree/");
|
|
204
|
+
expect(read("README.md")).toContain(".agents/skills/first-tree/");
|
|
205
|
+
expect(read("README.md")).toContain(".claude/skills/first-tree/");
|
|
202
206
|
expect(read("README.md")).toContain("bundled canonical");
|
|
207
|
+
expect(read("README.md")).toContain("dedicated tree repo");
|
|
203
208
|
expect(read("README.md")).toContain("`first-tree` skill");
|
|
204
209
|
expect(read("AGENTS.md")).toContain("references/source-map.md");
|
|
205
210
|
expect(read("AGENTS.md")).toContain("bundled skill path");
|
|
@@ -211,9 +216,12 @@ describe("skill artifacts", () => {
|
|
|
211
216
|
const onboarding = read("skills/first-tree/references/onboarding.md");
|
|
212
217
|
expect(onboarding).toContain("npx first-tree init");
|
|
213
218
|
expect(onboarding).toContain("npm install -g first-tree");
|
|
219
|
+
expect(onboarding).toContain("context-tree init --here");
|
|
214
220
|
expect(onboarding).toContain("installed CLI command is");
|
|
215
221
|
expect(onboarding).toContain("currently running `first-tree` npm package");
|
|
216
222
|
expect(onboarding).toContain("npx first-tree@latest upgrade");
|
|
223
|
+
expect(onboarding).toContain(".agents/skills/first-tree/");
|
|
224
|
+
expect(onboarding).toContain(".claude/skills/first-tree/");
|
|
217
225
|
expect(onboarding).not.toContain("This clones the framework into `.context-tree/`");
|
|
218
226
|
expect(onboarding).not.toContain("from upstream");
|
|
219
227
|
|
|
@@ -224,6 +232,8 @@ describe("skill artifacts", () => {
|
|
|
224
232
|
expect(skillMd).toContain("maintainer-testing.md");
|
|
225
233
|
expect(skillMd).toContain("currently running `first-tree` package");
|
|
226
234
|
expect(skillMd).toContain("so it is not confused with the `first-tree`");
|
|
235
|
+
expect(skillMd).toContain(".agents/skills/first-tree/");
|
|
236
|
+
expect(skillMd).toContain(".claude/skills/first-tree/");
|
|
227
237
|
expect(skillMd).not.toContain("canonical eval harness");
|
|
228
238
|
|
|
229
239
|
const sourceMap = read("skills/first-tree/references/source-map.md");
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdtempSync,
|
|
3
|
+
readFileSync,
|
|
4
|
+
rmSync,
|
|
5
|
+
symlinkSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
2
9
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
11
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
12
|
+
import { USAGE, isDirectExecution, runCli } from "../../../src/cli.ts";
|
|
5
13
|
|
|
6
|
-
const
|
|
14
|
+
const TEMP_DIRS: string[] = [];
|
|
7
15
|
|
|
8
16
|
function captureOutput(): { lines: string[]; write: (text: string) => void } {
|
|
9
17
|
const lines: string[] = [];
|
|
@@ -15,7 +23,45 @@ function captureOutput(): { lines: string[]; write: (text: string) => void } {
|
|
|
15
23
|
};
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
function makeTempDir(): string {
|
|
27
|
+
const dir = mkdtempSync(join(tmpdir(), "first-tree-thin-cli-"));
|
|
28
|
+
TEMP_DIRS.push(dir);
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
while (TEMP_DIRS.length > 0) {
|
|
34
|
+
const dir = TEMP_DIRS.pop();
|
|
35
|
+
if (dir) {
|
|
36
|
+
rmSync(dir, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
18
41
|
describe("thin CLI shell", () => {
|
|
42
|
+
it("treats a symlinked npm bin path as direct execution", () => {
|
|
43
|
+
const dir = makeTempDir();
|
|
44
|
+
const target = join(dir, "cli.js");
|
|
45
|
+
const symlinkPath = join(dir, "context-tree");
|
|
46
|
+
|
|
47
|
+
writeFileSync(target, "#!/usr/bin/env node\n");
|
|
48
|
+
symlinkSync(target, symlinkPath);
|
|
49
|
+
|
|
50
|
+
expect(isDirectExecution(symlinkPath, pathToFileURL(target).href)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not treat unrelated argv[1] values as direct execution", () => {
|
|
54
|
+
const dir = makeTempDir();
|
|
55
|
+
const target = join(dir, "cli.js");
|
|
56
|
+
const other = join(dir, "other.js");
|
|
57
|
+
|
|
58
|
+
writeFileSync(target, "#!/usr/bin/env node\n");
|
|
59
|
+
writeFileSync(other, "#!/usr/bin/env node\n");
|
|
60
|
+
|
|
61
|
+
expect(isDirectExecution(other, pathToFileURL(target).href)).toBe(false);
|
|
62
|
+
expect(isDirectExecution(undefined, pathToFileURL(target).href)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
19
65
|
it("prints usage with no args", async () => {
|
|
20
66
|
const output = captureOutput();
|
|
21
67
|
|
|
@@ -27,9 +73,8 @@ describe("thin CLI shell", () => {
|
|
|
27
73
|
|
|
28
74
|
it("prints the package version", async () => {
|
|
29
75
|
const output = captureOutput();
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
};
|
|
76
|
+
const pkgPath = fileURLToPath(new URL("../../../package.json", import.meta.url));
|
|
77
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version: string };
|
|
33
78
|
|
|
34
79
|
const code = await runCli(["--version"], output.write);
|
|
35
80
|
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
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 { runUpgrade } from "#skill/engine/upgrade.js";
|
|
6
6
|
import {
|
|
7
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
7
8
|
FRAMEWORK_VERSION,
|
|
8
9
|
INSTALLED_PROGRESS,
|
|
10
|
+
LEGACY_AGENT_INSTRUCTIONS_FILE,
|
|
9
11
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
10
12
|
import {
|
|
13
|
+
makeAgentsMd,
|
|
11
14
|
makeFramework,
|
|
15
|
+
makeSourceRepo,
|
|
12
16
|
makeLegacyFramework,
|
|
17
|
+
makeLegacyRepoFramework,
|
|
13
18
|
makeLegacyNamedFramework,
|
|
14
19
|
makeSourceSkill,
|
|
15
20
|
useTmpDir,
|
|
@@ -20,10 +25,7 @@ describe("runUpgrade", () => {
|
|
|
20
25
|
const repoDir = useTmpDir();
|
|
21
26
|
const sourceDir = useTmpDir();
|
|
22
27
|
makeLegacyFramework(repoDir.path, "0.1.0");
|
|
23
|
-
|
|
24
|
-
join(repoDir.path, "AGENT.md"),
|
|
25
|
-
"<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
|
|
26
|
-
);
|
|
28
|
+
makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
|
|
27
29
|
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
28
30
|
|
|
29
31
|
const result = runUpgrade(new Repo(repoDir.path), {
|
|
@@ -34,7 +36,13 @@ describe("runUpgrade", () => {
|
|
|
34
36
|
expect(existsSync(join(repoDir.path, ".context-tree"))).toBe(false);
|
|
35
37
|
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
36
38
|
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
37
|
-
"skills/first-tree/assets/framework/VERSION",
|
|
39
|
+
".agents/skills/first-tree/assets/framework/VERSION",
|
|
40
|
+
);
|
|
41
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
42
|
+
`Rename \`${LEGACY_AGENT_INSTRUCTIONS_FILE}\` to \`${AGENT_INSTRUCTIONS_FILE}\``,
|
|
43
|
+
);
|
|
44
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
45
|
+
"skills/first-tree/assets/framework/templates/agents.md.template",
|
|
38
46
|
);
|
|
39
47
|
});
|
|
40
48
|
|
|
@@ -72,6 +80,24 @@ describe("runUpgrade", () => {
|
|
|
72
80
|
);
|
|
73
81
|
});
|
|
74
82
|
|
|
83
|
+
it("migrates repos that still use the previous workspace skill path", () => {
|
|
84
|
+
const repoDir = useTmpDir();
|
|
85
|
+
const sourceDir = useTmpDir();
|
|
86
|
+
makeLegacyRepoFramework(repoDir.path, "0.1.0");
|
|
87
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
88
|
+
|
|
89
|
+
const result = runUpgrade(new Repo(repoDir.path), {
|
|
90
|
+
sourceRoot: sourceDir.path,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toBe(0);
|
|
94
|
+
expect(existsSync(join(repoDir.path, "skills", "first-tree"))).toBe(false);
|
|
95
|
+
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
|
|
96
|
+
expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
|
|
97
|
+
"skills/first-tree/",
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
75
101
|
it("refuses to replace a newer installed skill with an older packaged skill", () => {
|
|
76
102
|
const repoDir = useTmpDir();
|
|
77
103
|
const sourceDir = useTmpDir();
|
|
@@ -86,4 +112,11 @@ describe("runUpgrade", () => {
|
|
|
86
112
|
expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.3.0");
|
|
87
113
|
expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(false);
|
|
88
114
|
});
|
|
115
|
+
|
|
116
|
+
it("gives a dedicated-tree hint when run from a source repo", () => {
|
|
117
|
+
const repoDir = useTmpDir();
|
|
118
|
+
makeSourceRepo(repoDir.path);
|
|
119
|
+
const result = runUpgrade(new Repo(repoDir.path));
|
|
120
|
+
expect(result).toBe(1);
|
|
121
|
+
});
|
|
89
122
|
});
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { runInit } from "#skill/engine/init.js";
|
|
4
5
|
import { check, checkProgress, runVerify } from "#skill/engine/verify.js";
|
|
5
6
|
import { Repo } from "#skill/engine/repo.js";
|
|
6
7
|
import {
|
|
8
|
+
AGENT_INSTRUCTIONS_FILE,
|
|
7
9
|
INSTALLED_PROGRESS,
|
|
8
10
|
LEGACY_PROGRESS,
|
|
9
11
|
} from "#skill/engine/runtime/asset-loader.js";
|
|
10
12
|
import {
|
|
11
13
|
useTmpDir,
|
|
14
|
+
makeAgentsMd,
|
|
12
15
|
makeFramework,
|
|
13
16
|
makeLegacyFramework,
|
|
14
17
|
makeNode,
|
|
15
|
-
|
|
18
|
+
makeSourceRepo,
|
|
16
19
|
makeMembers,
|
|
20
|
+
makeSourceSkill,
|
|
17
21
|
} from "./helpers.js";
|
|
18
22
|
|
|
19
23
|
// --- check ---
|
|
@@ -39,7 +43,7 @@ describe("checkProgress", () => {
|
|
|
39
43
|
|
|
40
44
|
it("returns empty when all checked", () => {
|
|
41
45
|
const tmp = useTmpDir();
|
|
42
|
-
|
|
46
|
+
makeFramework(tmp.path);
|
|
43
47
|
writeFileSync(
|
|
44
48
|
join(tmp.path, INSTALLED_PROGRESS),
|
|
45
49
|
"# Progress\n- [x] Task one\n- [x] Task two\n",
|
|
@@ -50,7 +54,7 @@ describe("checkProgress", () => {
|
|
|
50
54
|
|
|
51
55
|
it("returns unchecked items", () => {
|
|
52
56
|
const tmp = useTmpDir();
|
|
53
|
-
|
|
57
|
+
makeFramework(tmp.path);
|
|
54
58
|
writeFileSync(
|
|
55
59
|
join(tmp.path, INSTALLED_PROGRESS),
|
|
56
60
|
"# Progress\n- [x] Done task\n- [ ] Pending task\n- [ ] Another pending\n",
|
|
@@ -61,7 +65,7 @@ describe("checkProgress", () => {
|
|
|
61
65
|
|
|
62
66
|
it("returns empty for empty progress", () => {
|
|
63
67
|
const tmp = useTmpDir();
|
|
64
|
-
|
|
68
|
+
makeFramework(tmp.path);
|
|
65
69
|
writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "");
|
|
66
70
|
const repo = new Repo(tmp.path);
|
|
67
71
|
expect(checkProgress(repo)).toEqual([]);
|
|
@@ -88,10 +92,7 @@ function buildFullRepo(root: string): void {
|
|
|
88
92
|
join(root, "NODE.md"),
|
|
89
93
|
"---\ntitle: My Org\nowners: [alice]\n---\n# Content\n",
|
|
90
94
|
);
|
|
91
|
-
|
|
92
|
-
join(root, "AGENT.md"),
|
|
93
|
-
"<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nstuff\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
|
|
94
|
-
);
|
|
95
|
+
makeAgentsMd(root, { markers: true });
|
|
95
96
|
makeMembers(root, 1);
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -108,6 +109,74 @@ describe("runVerify all passing", () => {
|
|
|
108
109
|
const ret = runVerify(repo, passValidator);
|
|
109
110
|
expect(ret).toBe(0);
|
|
110
111
|
});
|
|
112
|
+
|
|
113
|
+
it("passes after a real init flow when only the user tree remains to validate", () => {
|
|
114
|
+
const repoDir = useTmpDir();
|
|
115
|
+
const sourceDir = useTmpDir();
|
|
116
|
+
mkdirSync(join(repoDir.path, ".git"));
|
|
117
|
+
makeSourceSkill(sourceDir.path, "0.2.0");
|
|
118
|
+
|
|
119
|
+
expect(runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path })).toBe(0);
|
|
120
|
+
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(repoDir.path, "NODE.md"),
|
|
123
|
+
[
|
|
124
|
+
"---",
|
|
125
|
+
'title: "Example Tree"',
|
|
126
|
+
"owners: [alice]",
|
|
127
|
+
"---",
|
|
128
|
+
"",
|
|
129
|
+
"# Example Tree",
|
|
130
|
+
"",
|
|
131
|
+
"A repository initialized from the bundled skill for verification coverage.",
|
|
132
|
+
"",
|
|
133
|
+
"## Domains",
|
|
134
|
+
"",
|
|
135
|
+
"- **[members/](members/NODE.md)** — Team member definitions and responsibilities.",
|
|
136
|
+
"",
|
|
137
|
+
].join("\n"),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const agentPath = join(repoDir.path, AGENT_INSTRUCTIONS_FILE);
|
|
141
|
+
writeFileSync(
|
|
142
|
+
agentPath,
|
|
143
|
+
`${readFileSync(agentPath, "utf-8").trim()}\n\nProject-specific verification instructions.\n`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
mkdirSync(join(repoDir.path, "members", "alice"), { recursive: true });
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(repoDir.path, "members", "alice", "NODE.md"),
|
|
149
|
+
[
|
|
150
|
+
"---",
|
|
151
|
+
'title: "Alice"',
|
|
152
|
+
"owners: [alice]",
|
|
153
|
+
'type: "human"',
|
|
154
|
+
'role: "Maintainer"',
|
|
155
|
+
"domains:",
|
|
156
|
+
' - "members"',
|
|
157
|
+
"---",
|
|
158
|
+
"",
|
|
159
|
+
"# Alice",
|
|
160
|
+
"",
|
|
161
|
+
"## About",
|
|
162
|
+
"",
|
|
163
|
+
"Maintains the initialized tree and keeps the docs current.",
|
|
164
|
+
"",
|
|
165
|
+
"## Current Focus",
|
|
166
|
+
"",
|
|
167
|
+
"Validating the init-to-verify workflow.",
|
|
168
|
+
"",
|
|
169
|
+
].join("\n"),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const progressPath = join(repoDir.path, INSTALLED_PROGRESS);
|
|
173
|
+
writeFileSync(
|
|
174
|
+
progressPath,
|
|
175
|
+
readFileSync(progressPath, "utf-8").replace(/^- \[ \]/gm, "- [x]"),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(runVerify(new Repo(repoDir.path))).toBe(0);
|
|
179
|
+
});
|
|
111
180
|
});
|
|
112
181
|
|
|
113
182
|
// --- runVerify — failing checks ---
|
|
@@ -120,7 +189,7 @@ describe("runVerify failing", () => {
|
|
|
120
189
|
expect(ret).toBe(1);
|
|
121
190
|
});
|
|
122
191
|
|
|
123
|
-
it("fails when
|
|
192
|
+
it("fails when AGENTS.md is missing", () => {
|
|
124
193
|
const tmp = useTmpDir();
|
|
125
194
|
makeFramework(tmp.path);
|
|
126
195
|
writeFileSync(
|
|
@@ -132,6 +201,28 @@ describe("runVerify failing", () => {
|
|
|
132
201
|
expect(ret).toBe(1);
|
|
133
202
|
});
|
|
134
203
|
|
|
204
|
+
it("fails when only legacy AGENT.md exists", () => {
|
|
205
|
+
const tmp = useTmpDir();
|
|
206
|
+
mkdirSync(join(tmp.path, ".git"));
|
|
207
|
+
makeFramework(tmp.path);
|
|
208
|
+
makeNode(tmp.path);
|
|
209
|
+
makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
|
|
210
|
+
makeMembers(tmp.path, 1);
|
|
211
|
+
const repo = new Repo(tmp.path);
|
|
212
|
+
const ret = runVerify(repo, passValidator);
|
|
213
|
+
expect(existsSync(join(tmp.path, AGENT_INSTRUCTIONS_FILE))).toBe(false);
|
|
214
|
+
expect(ret).toBe(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("fails when legacy AGENT.md remains alongside AGENTS.md", () => {
|
|
218
|
+
const tmp = useTmpDir();
|
|
219
|
+
buildFullRepo(tmp.path);
|
|
220
|
+
makeAgentsMd(tmp.path, { legacyName: true, markers: true, userContent: true });
|
|
221
|
+
const repo = new Repo(tmp.path);
|
|
222
|
+
const ret = runVerify(repo, passValidator);
|
|
223
|
+
expect(ret).toBe(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
135
226
|
it("fails when node validation returns non-zero", () => {
|
|
136
227
|
const tmp = useTmpDir();
|
|
137
228
|
buildFullRepo(tmp.path);
|
|
@@ -139,4 +230,12 @@ describe("runVerify failing", () => {
|
|
|
139
230
|
const ret = runVerify(repo, failValidator);
|
|
140
231
|
expect(ret).toBe(1);
|
|
141
232
|
});
|
|
233
|
+
|
|
234
|
+
it("gives a dedicated-tree hint when run from a source repo", () => {
|
|
235
|
+
const tmp = useTmpDir();
|
|
236
|
+
makeSourceRepo(tmp.path);
|
|
237
|
+
const repo = new Repo(tmp.path);
|
|
238
|
+
const ret = runVerify(repo, passValidator);
|
|
239
|
+
expect(ret).toBe(1);
|
|
240
|
+
});
|
|
142
241
|
});
|
|
@@ -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 Git repository for your tree (separate from your code repos)\n- Node.js 18+\n- The npm package is `first-tree`, the installed CLI command is\n `context-tree`, and the installed skill directory in the tree is\n `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\n```bash\nmkdir my-org-tree && cd my-org-tree\ngit init\ncontext-tree init\n```\n\nThis installs the framework skill into `skills/first-tree/`, renders scaffolding (`NODE.md`, `AGENT.md`, `members/NODE.md`), and generates a task list in `skills/first-tree/progress.md`.\n\n### Step 2: Work Through the Task List\n\nRead `skills/first-tree/progress.md`. It contains a checklist tailored to 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 `AGENT.md` below the framework markers\n- Create member nodes under `members/`\n- Optionally configure agent integration (e.g., Claude Code session hooks)\n- Copy validation workflows from `skills/first-tree/assets/framework/workflows/` to `.github/workflows/`\n\nAs you complete each task, check it off in `skills/first-tree/progress.md` by changing `- [ ]` to `- [x]`.\n\n### Step 3: Verify\n\n```bash\ncontext-tree verify\n```\n\nThis fails if any items in `skills/first-tree/progress.md` remain unchecked, and runs deterministic checks (valid frontmatter, node structure, member 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` | Bootstrap a new tree. Installs the framework skill, renders templates, generates a task list. |\n| `context-tree verify` | Check the installed progress file for unchecked items + run deterministic validation. |\n| `context-tree upgrade` | Refresh the installed framework skill from the currently running `first-tree` npm package and generate follow-up tasks. |\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 `skills/first-tree/` from the\nskill bundled with the currently running `first-tree` npm package, preserves your\ntree content, and generates follow-up tasks in\n`skills/first-tree/progress.md`.\n\nIf your repo still uses the older `skills/first-tree-cli-framework/` path,\n`context-tree upgrade` will migrate it to `skills/first-tree/` 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- `skills/first-tree/references/principles.md` — Core principles with detailed examples\n- `skills/first-tree/references/ownership-and-naming.md` — How nodes are named and owned\n- `AGENT.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 };
|
package/dist/repo-BTJG8BU1.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { join, resolve } from "node:path";
|
|
3
|
-
const SKILL_ROOT = join("skills", "first-tree");
|
|
4
|
-
join(SKILL_ROOT, "agents");
|
|
5
|
-
join(SKILL_ROOT, "references");
|
|
6
|
-
const FRAMEWORK_ASSET_ROOT = join(SKILL_ROOT, "assets", "framework");
|
|
7
|
-
join(FRAMEWORK_ASSET_ROOT, "manifest.json");
|
|
8
|
-
const FRAMEWORK_VERSION = join(FRAMEWORK_ASSET_ROOT, "VERSION");
|
|
9
|
-
const FRAMEWORK_TEMPLATES_DIR = join(FRAMEWORK_ASSET_ROOT, "templates");
|
|
10
|
-
const FRAMEWORK_WORKFLOWS_DIR = join(FRAMEWORK_ASSET_ROOT, "workflows");
|
|
11
|
-
join(FRAMEWORK_ASSET_ROOT, "prompts");
|
|
12
|
-
const FRAMEWORK_EXAMPLES_DIR = join(FRAMEWORK_ASSET_ROOT, "examples");
|
|
13
|
-
join(FRAMEWORK_ASSET_ROOT, "helpers");
|
|
14
|
-
const INSTALLED_PROGRESS = join(SKILL_ROOT, "progress.md");
|
|
15
|
-
const LEGACY_SKILL_ROOT = join("skills", "first-tree-cli-framework");
|
|
16
|
-
const LEGACY_SKILL_ASSET_ROOT = join(LEGACY_SKILL_ROOT, "assets", "framework");
|
|
17
|
-
const LEGACY_SKILL_VERSION = join(LEGACY_SKILL_ASSET_ROOT, "VERSION");
|
|
18
|
-
join(LEGACY_SKILL_ASSET_ROOT, "templates");
|
|
19
|
-
join(LEGACY_SKILL_ASSET_ROOT, "workflows");
|
|
20
|
-
join(LEGACY_SKILL_ASSET_ROOT, "prompts");
|
|
21
|
-
join(LEGACY_SKILL_ASSET_ROOT, "examples");
|
|
22
|
-
const LEGACY_SKILL_PROGRESS = join(LEGACY_SKILL_ROOT, "progress.md");
|
|
23
|
-
const LEGACY_FRAMEWORK_ROOT = ".context-tree";
|
|
24
|
-
const LEGACY_VERSION = join(LEGACY_FRAMEWORK_ROOT, "VERSION");
|
|
25
|
-
const LEGACY_PROGRESS = join(LEGACY_FRAMEWORK_ROOT, "progress.md");
|
|
26
|
-
join(LEGACY_FRAMEWORK_ROOT, "templates");
|
|
27
|
-
join(LEGACY_FRAMEWORK_ROOT, "workflows");
|
|
28
|
-
join(LEGACY_FRAMEWORK_ROOT, "prompts");
|
|
29
|
-
join(LEGACY_FRAMEWORK_ROOT, "examples");
|
|
30
|
-
function pathExists(root, relPath) {
|
|
31
|
-
const fullPath = join(root, relPath);
|
|
32
|
-
try {
|
|
33
|
-
return existsSync(fullPath);
|
|
34
|
-
} catch {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
function frameworkVersionCandidates() {
|
|
39
|
-
return [
|
|
40
|
-
FRAMEWORK_VERSION,
|
|
41
|
-
LEGACY_SKILL_VERSION,
|
|
42
|
-
LEGACY_VERSION
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
function progressFileCandidates() {
|
|
46
|
-
return [
|
|
47
|
-
INSTALLED_PROGRESS,
|
|
48
|
-
LEGACY_SKILL_PROGRESS,
|
|
49
|
-
LEGACY_PROGRESS
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
function resolveFirstExistingPath(root, candidates) {
|
|
53
|
-
for (const candidate of candidates) if (pathExists(root, candidate)) return candidate;
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
function detectFrameworkLayout(root) {
|
|
57
|
-
if (pathExists(root, FRAMEWORK_VERSION)) return "skill";
|
|
58
|
-
if (pathExists(root, LEGACY_SKILL_VERSION)) return "legacy-skill";
|
|
59
|
-
if (pathExists(root, LEGACY_VERSION)) return "legacy";
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region skills/first-tree/engine/repo.ts
|
|
64
|
-
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
65
|
-
const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
|
|
66
|
-
const TITLE_RE = /^title:\s*['"]?(.+?)['"]?\s*$/m;
|
|
67
|
-
const FRAMEWORK_END_MARKER = "<!-- END CONTEXT-TREE FRAMEWORK -->";
|
|
68
|
-
var Repo = class {
|
|
69
|
-
root;
|
|
70
|
-
constructor(root) {
|
|
71
|
-
this.root = resolve(root ?? process.cwd());
|
|
72
|
-
}
|
|
73
|
-
pathExists(relPath) {
|
|
74
|
-
return existsSync(join(this.root, relPath));
|
|
75
|
-
}
|
|
76
|
-
fileContains(relPath, text) {
|
|
77
|
-
const fullPath = join(this.root, relPath);
|
|
78
|
-
try {
|
|
79
|
-
if (!statSync(fullPath).isFile()) return false;
|
|
80
|
-
return readFileSync(fullPath, "utf-8").includes(text);
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
readFile(relPath) {
|
|
86
|
-
try {
|
|
87
|
-
return readFileSync(join(this.root, relPath), "utf-8");
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
frontmatter(relPath) {
|
|
93
|
-
const text = this.readFile(relPath);
|
|
94
|
-
if (text === null) return null;
|
|
95
|
-
const m = text.match(FRONTMATTER_RE);
|
|
96
|
-
if (!m) return null;
|
|
97
|
-
const fm = m[1];
|
|
98
|
-
const result = {};
|
|
99
|
-
const titleM = fm.match(TITLE_RE);
|
|
100
|
-
if (titleM) result.title = titleM[1].trim();
|
|
101
|
-
const ownersM = fm.match(OWNERS_RE);
|
|
102
|
-
if (ownersM) {
|
|
103
|
-
const raw = ownersM[1].trim();
|
|
104
|
-
result.owners = raw ? raw.split(",").map((o) => o.trim()).filter(Boolean) : [];
|
|
105
|
-
}
|
|
106
|
-
return result.title !== void 0 || result.owners !== void 0 ? result : null;
|
|
107
|
-
}
|
|
108
|
-
anyAgentConfig() {
|
|
109
|
-
return [".claude/settings.json", ".codex/config.json"].some((c) => this.pathExists(c));
|
|
110
|
-
}
|
|
111
|
-
isGitRepo() {
|
|
112
|
-
try {
|
|
113
|
-
return statSync(join(this.root, ".git")).isDirectory();
|
|
114
|
-
} catch {
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
hasFramework() {
|
|
119
|
-
return this.frameworkLayout() !== null;
|
|
120
|
-
}
|
|
121
|
-
frameworkLayout() {
|
|
122
|
-
return detectFrameworkLayout(this.root);
|
|
123
|
-
}
|
|
124
|
-
readVersion() {
|
|
125
|
-
const versionPath = resolveFirstExistingPath(this.root, frameworkVersionCandidates());
|
|
126
|
-
if (versionPath === null) return null;
|
|
127
|
-
const text = this.readFile(versionPath);
|
|
128
|
-
return text ? text.trim() : null;
|
|
129
|
-
}
|
|
130
|
-
progressPath() {
|
|
131
|
-
return resolveFirstExistingPath(this.root, progressFileCandidates());
|
|
132
|
-
}
|
|
133
|
-
preferredProgressPath() {
|
|
134
|
-
const layout = this.frameworkLayout();
|
|
135
|
-
if (layout === "legacy") return LEGACY_PROGRESS;
|
|
136
|
-
if (layout === "legacy-skill") return LEGACY_SKILL_PROGRESS;
|
|
137
|
-
return INSTALLED_PROGRESS;
|
|
138
|
-
}
|
|
139
|
-
frameworkVersionPath() {
|
|
140
|
-
const layout = this.frameworkLayout();
|
|
141
|
-
if (layout === "legacy") return LEGACY_VERSION;
|
|
142
|
-
if (layout === "legacy-skill") return LEGACY_SKILL_VERSION;
|
|
143
|
-
return FRAMEWORK_VERSION;
|
|
144
|
-
}
|
|
145
|
-
hasAgentMdMarkers() {
|
|
146
|
-
const text = this.readFile("AGENT.md");
|
|
147
|
-
if (text === null) return false;
|
|
148
|
-
return text.includes("<!-- BEGIN CONTEXT-TREE FRAMEWORK") && text.includes("<!-- END CONTEXT-TREE FRAMEWORK -->");
|
|
149
|
-
}
|
|
150
|
-
hasMembers() {
|
|
151
|
-
const membersDir = join(this.root, "members");
|
|
152
|
-
try {
|
|
153
|
-
if (!statSync(membersDir).isDirectory()) return false;
|
|
154
|
-
} catch {
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
return existsSync(join(membersDir, "NODE.md"));
|
|
158
|
-
}
|
|
159
|
-
memberCount() {
|
|
160
|
-
const membersDir = join(this.root, "members");
|
|
161
|
-
try {
|
|
162
|
-
if (!statSync(membersDir).isDirectory()) return 0;
|
|
163
|
-
} catch {
|
|
164
|
-
return 0;
|
|
165
|
-
}
|
|
166
|
-
let count = 0;
|
|
167
|
-
const walk = (dir) => {
|
|
168
|
-
for (const entry of readdirSync(dir)) {
|
|
169
|
-
const childPath = join(dir, entry);
|
|
170
|
-
try {
|
|
171
|
-
if (!statSync(childPath).isDirectory()) continue;
|
|
172
|
-
} catch {
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (existsSync(join(childPath, "NODE.md"))) count++;
|
|
176
|
-
walk(childPath);
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
walk(membersDir);
|
|
180
|
-
return count;
|
|
181
|
-
}
|
|
182
|
-
hasPlaceholderNode() {
|
|
183
|
-
return this.fileContains("NODE.md", "<!-- PLACEHOLDER");
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
//#endregion
|
|
187
|
-
export { FRAMEWORK_TEMPLATES_DIR as a, INSTALLED_PROGRESS as c, SKILL_ROOT as d, FRAMEWORK_EXAMPLES_DIR as i, LEGACY_FRAMEWORK_ROOT as l, Repo as n, FRAMEWORK_VERSION as o, FRAMEWORK_ASSET_ROOT as r, FRAMEWORK_WORKFLOWS_DIR as s, FRAMEWORK_END_MARKER as t, LEGACY_SKILL_ROOT as u };
|