first-tree 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +116 -40
  2. package/dist/cli.js +46 -17
  3. package/dist/help-Dtdj91HJ.js +25 -0
  4. package/dist/init--VepFe6N.js +403 -0
  5. package/dist/installer-cH7N4RNj.js +47 -0
  6. package/dist/onboarding-C9cYSE6F.js +2 -0
  7. package/dist/onboarding-CPP8fF4D.js +10 -0
  8. package/dist/repo-DY57bMqr.js +318 -0
  9. package/dist/upgrade-Cgx_K2HM.js +135 -0
  10. package/dist/{verify-CSRIkuoM.js → verify-mC9ZTd1f.js} +118 -29
  11. package/package.json +33 -10
  12. package/skills/first-tree/SKILL.md +113 -0
  13. package/skills/first-tree/agents/openai.yaml +4 -0
  14. package/skills/first-tree/assets/framework/VERSION +1 -0
  15. package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
  16. package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
  17. package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
  18. package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
  19. package/skills/first-tree/assets/framework/helpers/run-review.ts +193 -0
  20. package/skills/first-tree/assets/framework/manifest.json +11 -0
  21. package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
  22. package/skills/first-tree/assets/framework/templates/agents.md.template +49 -0
  23. package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
  24. package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
  25. package/skills/first-tree/assets/framework/templates/root-node.md.template +41 -0
  26. package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
  27. package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
  28. package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
  29. package/skills/first-tree/engine/commands/help.ts +32 -0
  30. package/skills/first-tree/engine/commands/init.ts +1 -0
  31. package/skills/first-tree/engine/commands/upgrade.ts +1 -0
  32. package/skills/first-tree/engine/commands/verify.ts +1 -0
  33. package/skills/first-tree/engine/init.ts +414 -0
  34. package/skills/first-tree/engine/onboarding.ts +10 -0
  35. package/skills/first-tree/engine/repo.ts +360 -0
  36. package/skills/first-tree/engine/rules/agent-instructions.ts +59 -0
  37. package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
  38. package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
  39. package/skills/first-tree/engine/rules/framework.ts +13 -0
  40. package/skills/first-tree/engine/rules/index.ts +41 -0
  41. package/skills/first-tree/engine/rules/members.ts +21 -0
  42. package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
  43. package/skills/first-tree/engine/rules/root-node.ts +41 -0
  44. package/skills/first-tree/engine/runtime/adapters.ts +22 -0
  45. package/skills/first-tree/engine/runtime/asset-loader.ts +141 -0
  46. package/skills/first-tree/engine/runtime/installer.ts +82 -0
  47. package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
  48. package/skills/first-tree/engine/upgrade.ts +233 -0
  49. package/skills/first-tree/engine/validators/members.ts +215 -0
  50. package/skills/first-tree/engine/validators/nodes.ts +559 -0
  51. package/skills/first-tree/engine/verify.ts +155 -0
  52. package/skills/first-tree/references/about.md +36 -0
  53. package/skills/first-tree/references/maintainer-architecture.md +59 -0
  54. package/skills/first-tree/references/maintainer-build-and-distribution.md +59 -0
  55. package/skills/first-tree/references/maintainer-testing.md +58 -0
  56. package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
  57. package/skills/first-tree/references/onboarding.md +185 -0
  58. package/skills/first-tree/references/ownership-and-naming.md +94 -0
  59. package/skills/first-tree/references/principles.md +113 -0
  60. package/skills/first-tree/references/source-map.md +94 -0
  61. package/skills/first-tree/references/upgrade-contract.md +94 -0
  62. package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
  63. package/skills/first-tree/scripts/quick_validate.py +95 -0
  64. package/skills/first-tree/scripts/run-local-cli.sh +35 -0
  65. package/skills/first-tree/tests/asset-loader.test.ts +75 -0
  66. package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
  67. package/skills/first-tree/tests/helpers.ts +169 -0
  68. package/skills/first-tree/tests/init.test.ts +250 -0
  69. package/skills/first-tree/tests/repo.test.ts +440 -0
  70. package/skills/first-tree/tests/rules.test.ts +413 -0
  71. package/skills/first-tree/tests/run-review.test.ts +155 -0
  72. package/skills/first-tree/tests/skill-artifacts.test.ts +311 -0
  73. package/skills/first-tree/tests/thin-cli.test.ts +104 -0
  74. package/skills/first-tree/tests/upgrade.test.ts +103 -0
  75. package/skills/first-tree/tests/validate-members.test.ts +224 -0
  76. package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
  77. package/skills/first-tree/tests/verify.test.ts +241 -0
  78. package/dist/init-CE_944sb.js +0 -283
  79. package/dist/repo-BByc3VvM.js +0 -111
  80. package/dist/upgrade-Chr7z0CY.js +0 -82
@@ -0,0 +1,169 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { afterEach } from "vitest";
5
+ import {
6
+ AGENT_INSTRUCTIONS_FILE,
7
+ AGENT_INSTRUCTIONS_TEMPLATE,
8
+ FRAMEWORK_VERSION,
9
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
10
+ LEGACY_SKILL_VERSION,
11
+ LEGACY_VERSION,
12
+ } from "#skill/engine/runtime/asset-loader.js";
13
+
14
+ interface TmpDir {
15
+ path: string;
16
+ }
17
+
18
+ export function useTmpDir(): TmpDir {
19
+ const dir = mkdtempSync(join(tmpdir(), "ct-test-"));
20
+ afterEach(() => {
21
+ rmSync(dir, { recursive: true, force: true });
22
+ });
23
+ return { path: dir };
24
+ }
25
+
26
+ export function makeFramework(root: string, version = "0.1.0"): void {
27
+ mkdirSync(join(root, "skills", "first-tree", "assets", "framework"), {
28
+ recursive: true,
29
+ });
30
+ writeFileSync(join(root, FRAMEWORK_VERSION), `${version}\n`);
31
+ }
32
+
33
+ export function makeGitRepo(root: string): void {
34
+ mkdirSync(join(root, ".git"), { recursive: true });
35
+ }
36
+
37
+ export function makeSourceRepo(root: string): void {
38
+ makeGitRepo(root);
39
+ mkdirSync(join(root, "src"), { recursive: true });
40
+ writeFileSync(
41
+ join(root, "package.json"),
42
+ JSON.stringify({ name: "example-source-repo" }, null, 2),
43
+ );
44
+ writeFileSync(join(root, "src", "index.ts"), "export const ready = true;\n");
45
+ }
46
+
47
+ export function makeLegacyFramework(root: string, version = "0.1.0"): void {
48
+ const ct = join(root, ".context-tree");
49
+ mkdirSync(ct, { recursive: true });
50
+ writeFileSync(join(root, LEGACY_VERSION), `${version}\n`);
51
+ }
52
+
53
+ export function makeLegacyNamedFramework(
54
+ root: string,
55
+ version = "0.1.0",
56
+ ): void {
57
+ mkdirSync(
58
+ join(root, "skills", "first-tree-cli-framework", "assets", "framework"),
59
+ {
60
+ recursive: true,
61
+ },
62
+ );
63
+ writeFileSync(join(root, LEGACY_SKILL_VERSION), `${version}\n`);
64
+ }
65
+
66
+ export function makeSourceSkill(root: string, version = "0.2.0"): void {
67
+ const skillRoot = join(root, "skills", "first-tree");
68
+ mkdirSync(join(skillRoot, "agents"), { recursive: true });
69
+ mkdirSync(join(skillRoot, "assets", "framework", "templates"), {
70
+ recursive: true,
71
+ });
72
+
73
+ writeFileSync(
74
+ join(skillRoot, "SKILL.md"),
75
+ "---\nname: first-tree\ndescription: test\n---\n",
76
+ );
77
+ writeFileSync(
78
+ join(skillRoot, "agents", "openai.yaml"),
79
+ "display_name: First Tree\nshort_description: test\n",
80
+ );
81
+ writeFileSync(
82
+ join(skillRoot, "assets", "framework", "manifest.json"),
83
+ "{}\n",
84
+ );
85
+ writeFileSync(
86
+ join(skillRoot, "assets", "framework", "VERSION"),
87
+ `${version}\n`,
88
+ );
89
+ writeFileSync(
90
+ join(
91
+ skillRoot,
92
+ "assets",
93
+ "framework",
94
+ "templates",
95
+ "root-node.md.template",
96
+ ),
97
+ "---\ntitle: Example Tree\nowners: [alice]\n---\n# Example Tree\n",
98
+ );
99
+ writeFileSync(
100
+ join(
101
+ skillRoot,
102
+ "assets",
103
+ "framework",
104
+ "templates",
105
+ AGENT_INSTRUCTIONS_TEMPLATE,
106
+ ),
107
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nframework text\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
108
+ );
109
+ writeFileSync(
110
+ join(
111
+ skillRoot,
112
+ "assets",
113
+ "framework",
114
+ "templates",
115
+ "members-domain.md.template",
116
+ ),
117
+ "---\ntitle: Members\nowners: [alice]\n---\n# Members\n",
118
+ );
119
+ }
120
+
121
+ export function makeNode(
122
+ root: string,
123
+ opts?: { placeholder?: boolean },
124
+ ): void {
125
+ const body = opts?.placeholder
126
+ ? "<!-- PLACEHOLDER -->\n"
127
+ : "# Real content\n";
128
+ writeFileSync(
129
+ join(root, "NODE.md"),
130
+ `---\ntitle: My Org\nowners: [alice]\n---\n${body}`,
131
+ );
132
+ }
133
+
134
+ export function makeAgentsMd(
135
+ root: string,
136
+ opts?: { markers?: boolean; userContent?: boolean; legacyName?: boolean },
137
+ ): void {
138
+ const markers = opts?.markers ?? true;
139
+ const userContent = opts?.userContent ?? false;
140
+ const fileName = opts?.legacyName
141
+ ? LEGACY_AGENT_INSTRUCTIONS_FILE
142
+ : AGENT_INSTRUCTIONS_FILE;
143
+ const parts: string[] = [];
144
+ if (markers) {
145
+ parts.push(
146
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nframework stuff\n<!-- END CONTEXT-TREE FRAMEWORK -->",
147
+ );
148
+ } else {
149
+ parts.push("# Agent instructions\n");
150
+ }
151
+ if (userContent) {
152
+ parts.push("\n# Project-specific\nThis is real user content.\n");
153
+ }
154
+ writeFileSync(join(root, fileName), parts.join("\n"));
155
+ }
156
+
157
+ export function makeMembers(root: string, count = 1): void {
158
+ const membersDir = join(root, "members");
159
+ mkdirSync(membersDir, { recursive: true });
160
+ writeFileSync(
161
+ join(membersDir, "NODE.md"),
162
+ "---\ntitle: Members\n---\n",
163
+ );
164
+ for (let i = 0; i < count; i++) {
165
+ const d = join(membersDir, `member-${i}`);
166
+ mkdirSync(d);
167
+ writeFileSync(join(d, "NODE.md"), `---\ntitle: Member ${i}\nowners: [member-${i}]\ntype: human\nrole: Engineer\ndomains:\n - engineering\n---\n`);
168
+ }
169
+ }
@@ -0,0 +1,250 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ formatTaskList,
6
+ parseInitArgs,
7
+ writeProgress,
8
+ runInit,
9
+ } from "#skill/engine/init.js";
10
+ import { Repo } from "#skill/engine/repo.js";
11
+ import {
12
+ AGENT_INSTRUCTIONS_FILE,
13
+ FRAMEWORK_VERSION,
14
+ INSTALLED_PROGRESS,
15
+ LEGACY_AGENT_INSTRUCTIONS_FILE,
16
+ LEGACY_PROGRESS,
17
+ } from "#skill/engine/runtime/asset-loader.js";
18
+ import {
19
+ makeGitRepo,
20
+ useTmpDir,
21
+ makeAgentsMd,
22
+ makeFramework,
23
+ makeLegacyFramework,
24
+ makeSourceRepo,
25
+ makeSourceSkill,
26
+ } from "./helpers.js";
27
+
28
+ // --- formatTaskList ---
29
+
30
+ describe("formatTaskList", () => {
31
+ it("produces markdown heading", () => {
32
+ const groups = [
33
+ { group: "Framework", order: 1, tasks: ["Install framework"] },
34
+ ];
35
+ const output = formatTaskList(groups);
36
+ expect(output).toMatch(/^# Context Tree Init/);
37
+ });
38
+
39
+ it("includes group heading", () => {
40
+ const groups = [
41
+ { group: "Framework", order: 1, tasks: ["Install framework"] },
42
+ ];
43
+ const output = formatTaskList(groups);
44
+ expect(output).toContain("## Framework");
45
+ });
46
+
47
+ it("includes task as checkbox", () => {
48
+ const groups = [
49
+ { group: "Root Node", order: 2, tasks: ["Fix title"] },
50
+ ];
51
+ const output = formatTaskList(groups);
52
+ expect(output).toContain("- [ ] Fix title");
53
+ });
54
+
55
+ it("handles multiple groups", () => {
56
+ const groups = [
57
+ { group: "A", order: 1, tasks: ["task-a1", "task-a2"] },
58
+ { group: "B", order: 2, tasks: ["task-b1"] },
59
+ ];
60
+ const output = formatTaskList(groups);
61
+ expect(output).toContain("## A");
62
+ expect(output).toContain("## B");
63
+ expect(output).toContain("- [ ] task-a1");
64
+ expect(output).toContain("- [ ] task-a2");
65
+ expect(output).toContain("- [ ] task-b1");
66
+ });
67
+
68
+ it("includes verification section", () => {
69
+ const groups = [{ group: "G", order: 1, tasks: ["t"] }];
70
+ const output = formatTaskList(groups);
71
+ expect(output).toContain("## Verification");
72
+ expect(output).toContain("context-tree verify");
73
+ });
74
+
75
+ it("handles empty groups", () => {
76
+ const output = formatTaskList([]);
77
+ expect(output).toContain("# Context Tree Init");
78
+ expect(output).toContain("## Verification");
79
+ });
80
+ });
81
+
82
+ // --- writeProgress ---
83
+
84
+ describe("writeProgress", () => {
85
+ it("writes to correct path", () => {
86
+ const tmp = useTmpDir();
87
+ const repo = new Repo(tmp.path);
88
+ writeProgress(repo, "# hello\n");
89
+ const progress = join(tmp.path, INSTALLED_PROGRESS);
90
+ expect(readFileSync(progress, "utf-8")).toBe("# hello\n");
91
+ });
92
+
93
+ it("creates directory if missing", () => {
94
+ const tmp = useTmpDir();
95
+ const repo = new Repo(tmp.path);
96
+ writeProgress(repo, "content");
97
+ const progress = join(tmp.path, INSTALLED_PROGRESS);
98
+ expect(readFileSync(progress, "utf-8")).toBe("content");
99
+ });
100
+
101
+ it("overwrites existing file", () => {
102
+ const tmp = useTmpDir();
103
+ mkdirSync(join(tmp.path, "skills", "first-tree"), {
104
+ recursive: true,
105
+ });
106
+ writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "old");
107
+ const repo = new Repo(tmp.path);
108
+ writeProgress(repo, "new");
109
+ expect(readFileSync(join(tmp.path, INSTALLED_PROGRESS), "utf-8")).toBe("new");
110
+ });
111
+
112
+ it("keeps using the legacy progress path for legacy repos", () => {
113
+ const tmp = useTmpDir();
114
+ makeLegacyFramework(tmp.path);
115
+ const repo = new Repo(tmp.path);
116
+ writeProgress(repo, "legacy");
117
+ expect(readFileSync(join(tmp.path, LEGACY_PROGRESS), "utf-8")).toBe("legacy");
118
+ });
119
+ });
120
+
121
+ // --- runInit — guard logic (no network) ---
122
+
123
+ const fakeGitInitializer = (root: string): void => {
124
+ makeGitRepo(root);
125
+ };
126
+
127
+ describe("runInit", () => {
128
+ it("errors when not a git repo", () => {
129
+ const tmp = useTmpDir();
130
+ const repo = new Repo(tmp.path);
131
+ const ret = runInit(repo);
132
+ expect(ret).toBe(1);
133
+ });
134
+
135
+ it("installs the bundled skill and scaffolding when framework is missing", () => {
136
+ const repoDir = useTmpDir();
137
+ const sourceDir = useTmpDir();
138
+ makeGitRepo(repoDir.path);
139
+ makeSourceSkill(sourceDir.path, "0.2.0");
140
+
141
+ const ret = runInit(new Repo(repoDir.path), {
142
+ sourceRoot: sourceDir.path,
143
+ gitInitializer: fakeGitInitializer,
144
+ });
145
+
146
+ expect(ret).toBe(0);
147
+ expect(
148
+ existsSync(join(repoDir.path, "skills", "first-tree", "SKILL.md")),
149
+ ).toBe(true);
150
+ expect(readFileSync(join(repoDir.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.2.0");
151
+ expect(existsSync(join(repoDir.path, "NODE.md"))).toBe(true);
152
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
153
+ expect(existsSync(join(repoDir.path, "members", "NODE.md"))).toBe(true);
154
+ expect(existsSync(join(repoDir.path, INSTALLED_PROGRESS))).toBe(true);
155
+ });
156
+
157
+ it("does not scaffold AGENTS.md when legacy AGENT.md already exists", () => {
158
+ const repoDir = useTmpDir();
159
+ const sourceDir = useTmpDir();
160
+ mkdirSync(join(repoDir.path, ".git"));
161
+ makeAgentsMd(repoDir.path, { legacyName: true, markers: true, userContent: true });
162
+ makeSourceSkill(sourceDir.path, "0.2.0");
163
+
164
+ const ret = runInit(new Repo(repoDir.path), { sourceRoot: sourceDir.path });
165
+
166
+ expect(ret).toBe(0);
167
+ expect(existsSync(join(repoDir.path, LEGACY_AGENT_INSTRUCTIONS_FILE))).toBe(true);
168
+ expect(existsSync(join(repoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(false);
169
+ expect(readFileSync(join(repoDir.path, INSTALLED_PROGRESS), "utf-8")).toContain(
170
+ "Rename `AGENT.md` to `AGENTS.md`",
171
+ );
172
+ });
173
+
174
+ it("skips reinstall when framework exists", () => {
175
+ const tmp = useTmpDir();
176
+ const sourceDir = useTmpDir();
177
+ makeGitRepo(tmp.path);
178
+ makeFramework(tmp.path, "0.1.0");
179
+ makeSourceSkill(sourceDir.path, "0.2.0");
180
+
181
+ const repo = new Repo(tmp.path);
182
+ const ret = runInit(repo, { sourceRoot: sourceDir.path });
183
+
184
+ expect(ret).toBe(0);
185
+ expect(readFileSync(join(tmp.path, FRAMEWORK_VERSION), "utf-8").trim()).toBe("0.1.0");
186
+ });
187
+
188
+ it("creates a sibling tree repo by default when invoked from a source repo", () => {
189
+ const sourceRepoDir = useTmpDir();
190
+ const sourceSkillDir = useTmpDir();
191
+ makeSourceRepo(sourceRepoDir.path);
192
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
193
+
194
+ const ret = runInit(new Repo(sourceRepoDir.path), {
195
+ sourceRoot: sourceSkillDir.path,
196
+ gitInitializer: fakeGitInitializer,
197
+ });
198
+
199
+ const treeRepo = join(
200
+ dirname(sourceRepoDir.path),
201
+ `${basename(sourceRepoDir.path)}-context`,
202
+ );
203
+
204
+ expect(ret).toBe(0);
205
+ expect(existsSync(join(treeRepo, "skills", "first-tree", "SKILL.md"))).toBe(true);
206
+ expect(existsSync(join(treeRepo, "NODE.md"))).toBe(true);
207
+ expect(existsSync(join(treeRepo, AGENT_INSTRUCTIONS_FILE))).toBe(true);
208
+ expect(existsSync(join(treeRepo, "members", "NODE.md"))).toBe(true);
209
+ expect(existsSync(join(treeRepo, INSTALLED_PROGRESS))).toBe(true);
210
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(false);
211
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(false);
212
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(false);
213
+ });
214
+
215
+ it("keeps supporting in-place init with --here", () => {
216
+ const sourceRepoDir = useTmpDir();
217
+ const sourceSkillDir = useTmpDir();
218
+ makeSourceRepo(sourceRepoDir.path);
219
+ makeSourceSkill(sourceSkillDir.path, "0.2.0");
220
+
221
+ const ret = runInit(new Repo(sourceRepoDir.path), {
222
+ here: true,
223
+ sourceRoot: sourceSkillDir.path,
224
+ gitInitializer: fakeGitInitializer,
225
+ });
226
+
227
+ expect(ret).toBe(0);
228
+ expect(existsSync(join(sourceRepoDir.path, "NODE.md"))).toBe(true);
229
+ expect(existsSync(join(sourceRepoDir.path, AGENT_INSTRUCTIONS_FILE))).toBe(true);
230
+ expect(existsSync(join(sourceRepoDir.path, "members", "NODE.md"))).toBe(true);
231
+ expect(existsSync(join(sourceRepoDir.path, INSTALLED_PROGRESS))).toBe(true);
232
+ });
233
+ });
234
+
235
+ describe("parseInitArgs", () => {
236
+ it("parses dedicated repo options", () => {
237
+ expect(parseInitArgs(["--tree-name", "acme-context"])).toEqual({
238
+ treeName: "acme-context",
239
+ });
240
+ expect(parseInitArgs(["--tree-path", "../acme-context"])).toEqual({
241
+ treePath: "../acme-context",
242
+ });
243
+ });
244
+
245
+ it("rejects incompatible init options", () => {
246
+ expect(parseInitArgs(["--here", "--tree-name", "acme-context"])).toEqual({
247
+ error: "Cannot combine --here with --tree-name",
248
+ });
249
+ });
250
+ });