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,224 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ extractScalar,
6
+ extractList,
7
+ validateMember,
8
+ runValidateMembers,
9
+ } from "#skill/engine/validators/members.js";
10
+ import { useTmpDir } from "./helpers.js";
11
+
12
+ function write(root: string, relPath: string, content: string): string {
13
+ const p = join(root, relPath);
14
+ mkdirSync(join(p, ".."), { recursive: true });
15
+ writeFileSync(p, content);
16
+ return p;
17
+ }
18
+
19
+ const VALID_MEMBER = `---
20
+ title: Alice
21
+ owners: [alice]
22
+ type: human
23
+ role: Engineer
24
+ domains: [engineering]
25
+ ---
26
+ # Alice
27
+ `;
28
+
29
+ const VALID_ASSISTANT = `---
30
+ title: Alice Assistant
31
+ owners: [alice]
32
+ type: personal_assistant
33
+ role: Assistant
34
+ domains: [engineering]
35
+ ---
36
+ # Alice Assistant
37
+ `;
38
+
39
+ // --- validateMember ---
40
+
41
+ describe("validateMember", () => {
42
+ it("accepts valid member", () => {
43
+ const tmp = useTmpDir();
44
+ const p = write(tmp.path, "members/alice/NODE.md", VALID_MEMBER);
45
+ expect(validateMember(p, tmp.path)).toEqual([]);
46
+ });
47
+
48
+ it("reports missing title", () => {
49
+ const tmp = useTmpDir();
50
+ const content = "---\nowners: [alice]\ntype: human\nrole: Eng\ndomains: [eng]\n---\n";
51
+ const p = write(tmp.path, "members/alice/NODE.md", content);
52
+ const errors = validateMember(p, tmp.path);
53
+ expect(errors.some((e) => e.includes("title"))).toBe(true);
54
+ });
55
+
56
+ it("reports missing type", () => {
57
+ const tmp = useTmpDir();
58
+ const content = "---\ntitle: Alice\nowners: [alice]\nrole: Eng\ndomains: [eng]\n---\n";
59
+ const p = write(tmp.path, "members/alice/NODE.md", content);
60
+ const errors = validateMember(p, tmp.path);
61
+ expect(errors.some((e) => e.includes("type"))).toBe(true);
62
+ });
63
+
64
+ it("reports invalid type", () => {
65
+ const tmp = useTmpDir();
66
+ const content = "---\ntitle: Alice\nowners: [alice]\ntype: robot\nrole: Eng\ndomains: [eng]\n---\n";
67
+ const p = write(tmp.path, "members/alice/NODE.md", content);
68
+ const errors = validateMember(p, tmp.path);
69
+ expect(errors.some((e) => e.includes("invalid type"))).toBe(true);
70
+ });
71
+
72
+ it("reports missing domains", () => {
73
+ const tmp = useTmpDir();
74
+ const content = "---\ntitle: Alice\nowners: [alice]\ntype: human\nrole: Eng\n---\n";
75
+ const p = write(tmp.path, "members/alice/NODE.md", content);
76
+ const errors = validateMember(p, tmp.path);
77
+ expect(errors.some((e) => e.includes("domains"))).toBe(true);
78
+ });
79
+ });
80
+
81
+ // --- runValidateMembers: recursive scanning ---
82
+
83
+ describe("runValidateMembers", () => {
84
+ it("validates nested members recursively", () => {
85
+ const tmp = useTmpDir();
86
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
87
+ write(tmp.path, "members/alice/NODE.md", VALID_MEMBER);
88
+ write(tmp.path, "members/team-a/NODE.md", `---\ntitle: Team A\nowners: [lead]\ntype: autonomous_agent\nrole: Team Lead\ndomains: [engineering]\n---\n`);
89
+ write(tmp.path, "members/team-a/bot-1/NODE.md", `---\ntitle: Bot 1\nowners: [lead]\ntype: autonomous_agent\nrole: Worker\ndomains: [engineering]\n---\n`);
90
+
91
+ const result = runValidateMembers(tmp.path);
92
+ expect(result.exitCode).toBe(0);
93
+ expect(result.errors).toEqual([]);
94
+ });
95
+
96
+ it("detects duplicate directory names across levels", () => {
97
+ const tmp = useTmpDir();
98
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
99
+ write(tmp.path, "members/alice/NODE.md", VALID_MEMBER);
100
+ // Same name "alice" nested under team-a
101
+ write(tmp.path, "members/team-a/NODE.md", `---\ntitle: Team A\nowners: [lead]\ntype: autonomous_agent\nrole: Lead\ndomains: [eng]\n---\n`);
102
+ write(tmp.path, "members/team-a/alice/NODE.md", `---\ntitle: Alice Clone\nowners: [alice2]\ntype: human\nrole: Eng\ndomains: [eng]\n---\n`);
103
+
104
+ const result = runValidateMembers(tmp.path);
105
+ expect(result.exitCode).toBe(1);
106
+ expect(result.errors.some((e) => e.includes("Duplicate member directory name 'alice'"))).toBe(true);
107
+ });
108
+
109
+ it("reports missing NODE.md in nested directories", () => {
110
+ const tmp = useTmpDir();
111
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
112
+ write(tmp.path, "members/alice/NODE.md", VALID_MEMBER);
113
+ // Create directory without NODE.md
114
+ mkdirSync(join(tmp.path, "members/team-a/orphan"), { recursive: true });
115
+ write(tmp.path, "members/team-a/NODE.md", `---\ntitle: Team A\nowners: [lead]\ntype: autonomous_agent\nrole: Lead\ndomains: [eng]\n---\n`);
116
+
117
+ const result = runValidateMembers(tmp.path);
118
+ expect(result.exitCode).toBe(1);
119
+ expect(result.errors.some((e) => e.includes("orphan") && e.includes("missing NODE.md"))).toBe(true);
120
+ });
121
+
122
+ it("validates deeply nested members (3+ levels)", () => {
123
+ const tmp = useTmpDir();
124
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
125
+ write(tmp.path, "members/org/NODE.md", `---\ntitle: Org\nowners: [admin]\ntype: autonomous_agent\nrole: Org\ndomains: [all]\n---\n`);
126
+ write(tmp.path, "members/org/team-a/NODE.md", `---\ntitle: Team A\nowners: [lead]\ntype: autonomous_agent\nrole: Lead\ndomains: [eng]\n---\n`);
127
+ write(tmp.path, "members/org/team-a/bot-1/NODE.md", `---\ntitle: Bot 1\nowners: [lead]\ntype: autonomous_agent\nrole: Worker\ndomains: [eng]\n---\n`);
128
+
129
+ const result = runValidateMembers(tmp.path);
130
+ expect(result.exitCode).toBe(0);
131
+ });
132
+ });
133
+
134
+ // --- runValidateMembers: delegate_mention cross-validation ---
135
+
136
+ describe("runValidateMembers delegate_mention", () => {
137
+ it("accepts valid delegate_mention to personal_assistant", () => {
138
+ const tmp = useTmpDir();
139
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
140
+ write(tmp.path, "members/alice/NODE.md", `---\ntitle: Alice\nowners: [alice]\ntype: human\nrole: Eng\ndomains: [eng]\ndelegate_mention: alice-assistant\n---\n`);
141
+ write(tmp.path, "members/alice-assistant/NODE.md", VALID_ASSISTANT);
142
+
143
+ const result = runValidateMembers(tmp.path);
144
+ expect(result.exitCode).toBe(0);
145
+ expect(result.errors).toEqual([]);
146
+ });
147
+
148
+ it("rejects delegate_mention to non-existent member", () => {
149
+ const tmp = useTmpDir();
150
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
151
+ write(tmp.path, "members/alice/NODE.md", `---\ntitle: Alice\nowners: [alice]\ntype: human\nrole: Eng\ndomains: [eng]\ndelegate_mention: ghost\n---\n`);
152
+
153
+ const result = runValidateMembers(tmp.path);
154
+ expect(result.exitCode).toBe(1);
155
+ expect(result.errors.some((e) => e.includes("delegate_mention 'ghost'") && e.includes("non-existent"))).toBe(true);
156
+ });
157
+
158
+ it("rejects delegate_mention to non-personal_assistant member", () => {
159
+ const tmp = useTmpDir();
160
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
161
+ write(tmp.path, "members/alice/NODE.md", `---\ntitle: Alice\nowners: [alice]\ntype: human\nrole: Eng\ndomains: [eng]\ndelegate_mention: bob\n---\n`);
162
+ write(tmp.path, "members/bob/NODE.md", `---\ntitle: Bob\nowners: [bob]\ntype: human\nrole: Eng\ndomains: [eng]\n---\n`);
163
+
164
+ const result = runValidateMembers(tmp.path);
165
+ expect(result.exitCode).toBe(1);
166
+ expect(result.errors.some((e) => e.includes("delegate_mention 'bob'") && e.includes("personal_assistant"))).toBe(true);
167
+ });
168
+
169
+ it("accepts delegate_mention to nested personal_assistant", () => {
170
+ const tmp = useTmpDir();
171
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
172
+ write(tmp.path, "members/alice/NODE.md", `---\ntitle: Alice\nowners: [alice]\ntype: human\nrole: Eng\ndomains: [eng]\ndelegate_mention: helper\n---\n`);
173
+ write(tmp.path, "members/bots/NODE.md", `---\ntitle: Bots\nowners: [admin]\ntype: autonomous_agent\nrole: Group\ndomains: [infra]\n---\n`);
174
+ write(tmp.path, "members/bots/helper/NODE.md", `---\ntitle: Helper\nowners: [admin]\ntype: personal_assistant\nrole: Assistant\ndomains: [eng]\n---\n`);
175
+
176
+ const result = runValidateMembers(tmp.path);
177
+ expect(result.exitCode).toBe(0);
178
+ });
179
+
180
+ it("allows member without delegate_mention", () => {
181
+ const tmp = useTmpDir();
182
+ write(tmp.path, "members/NODE.md", "---\ntitle: Members\n---\n");
183
+ write(tmp.path, "members/alice/NODE.md", VALID_MEMBER);
184
+
185
+ const result = runValidateMembers(tmp.path);
186
+ expect(result.exitCode).toBe(0);
187
+ });
188
+ });
189
+
190
+ // --- extractScalar ---
191
+
192
+ describe("extractScalar", () => {
193
+ it("extracts regular value", () => {
194
+ expect(extractScalar("title: Hello World\nowners: [a]", "title")).toBe("Hello World");
195
+ });
196
+
197
+ it("extracts quoted value", () => {
198
+ expect(extractScalar('title: "Hello World"\nowners: [a]', "title")).toBe("Hello World");
199
+ });
200
+
201
+ it("returns null for missing key", () => {
202
+ expect(extractScalar("owners: [a]", "title")).toBeNull();
203
+ });
204
+ });
205
+
206
+ // --- extractList ---
207
+
208
+ describe("extractList", () => {
209
+ it("extracts inline list", () => {
210
+ expect(extractList("domains: [eng, product]", "domains")).toEqual(["eng", "product"]);
211
+ });
212
+
213
+ it("extracts block list", () => {
214
+ expect(extractList("domains:\n - eng\n - product\n", "domains")).toEqual(["eng", "product"]);
215
+ });
216
+
217
+ it("handles empty list", () => {
218
+ expect(extractList("domains: []", "domains")).toEqual([]);
219
+ });
220
+
221
+ it("returns null for missing key", () => {
222
+ expect(extractList("owners: [a]", "domains")).toBeNull();
223
+ });
224
+ });
@@ -0,0 +1,198 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import {
5
+ Findings,
6
+ parseFrontmatter,
7
+ parseSoftLinks,
8
+ validateOwners,
9
+ validateFolders,
10
+ validateEmptyNodes,
11
+ validateTitleMismatch,
12
+ setTreeRoot,
13
+ } from "#skill/engine/validators/nodes.js";
14
+ import { useTmpDir } from "./helpers.js";
15
+
16
+ function write(root: string, relPath: string, content: string): string {
17
+ const p = join(root, relPath);
18
+ mkdirSync(join(p, ".."), { recursive: true });
19
+ writeFileSync(p, content);
20
+ return p;
21
+ }
22
+
23
+ function setup(tmp: { path: string }): string {
24
+ setTreeRoot(tmp.path);
25
+ return tmp.path;
26
+ }
27
+
28
+ // --- parseFrontmatter ---
29
+
30
+ describe("parseFrontmatter", () => {
31
+ it("parses valid frontmatter", () => {
32
+ const tmp = useTmpDir();
33
+ const root = setup(tmp);
34
+ const p = write(root, "NODE.md", "---\ntitle: Hello\nowners: [alice]\n---\n# Hello\n");
35
+ const fm = parseFrontmatter(p);
36
+ expect(fm).not.toBeNull();
37
+ expect(fm).toContain("title: Hello");
38
+ });
39
+
40
+ it("returns null for missing frontmatter", () => {
41
+ const tmp = useTmpDir();
42
+ const root = setup(tmp);
43
+ const p = write(root, "NODE.md", "# No frontmatter here\n");
44
+ expect(parseFrontmatter(p)).toBeNull();
45
+ });
46
+
47
+ it("returns null for malformed frontmatter", () => {
48
+ const tmp = useTmpDir();
49
+ const root = setup(tmp);
50
+ const p = write(root, "NODE.md", "---\ntitle: Oops\nNo closing fence\n");
51
+ expect(parseFrontmatter(p)).toBeNull();
52
+ });
53
+ });
54
+
55
+ // --- parseSoftLinks ---
56
+
57
+ describe("parseSoftLinks", () => {
58
+ it("parses inline format", () => {
59
+ expect(parseSoftLinks("owners: [alice]\nsoft_links: [/a, /b]")).toEqual(["/a", "/b"]);
60
+ });
61
+
62
+ it("parses block format", () => {
63
+ expect(parseSoftLinks("owners: [alice]\nsoft_links:\n - /x\n - /y\n")).toEqual(["/x", "/y"]);
64
+ });
65
+
66
+ it("handles empty inline", () => {
67
+ expect(parseSoftLinks("owners: [alice]\nsoft_links: []")).toEqual([]);
68
+ });
69
+
70
+ it("returns null when missing", () => {
71
+ expect(parseSoftLinks("owners: [alice]")).toBeNull();
72
+ });
73
+ });
74
+
75
+ // --- validateOwners ---
76
+
77
+ describe("validateOwners", () => {
78
+ it("accepts valid owners", () => {
79
+ const tmp = useTmpDir();
80
+ const root = setup(tmp);
81
+ const p = write(root, "NODE.md", "---\nowners: [alice, bob]\n---\n");
82
+ const fm = parseFrontmatter(p)!;
83
+ const f = new Findings();
84
+ validateOwners(fm, p, f);
85
+ expect(f.errors).toEqual([]);
86
+ });
87
+
88
+ it("accepts wildcard", () => {
89
+ const tmp = useTmpDir();
90
+ const root = setup(tmp);
91
+ const p = write(root, "NODE.md", "---\nowners: [*]\n---\n");
92
+ const fm = parseFrontmatter(p)!;
93
+ const f = new Findings();
94
+ validateOwners(fm, p, f);
95
+ expect(f.errors).toEqual([]);
96
+ });
97
+
98
+ it("accepts empty inheritance", () => {
99
+ const tmp = useTmpDir();
100
+ const root = setup(tmp);
101
+ const p = write(root, "NODE.md", "---\nowners: []\n---\n");
102
+ const fm = parseFrontmatter(p)!;
103
+ const f = new Findings();
104
+ validateOwners(fm, p, f);
105
+ expect(f.errors).toEqual([]);
106
+ });
107
+
108
+ it("rejects invalid username", () => {
109
+ const tmp = useTmpDir();
110
+ const root = setup(tmp);
111
+ const p = write(root, "NODE.md", "---\nowners: [not valid!]\n---\n");
112
+ const fm = parseFrontmatter(p)!;
113
+ const f = new Findings();
114
+ validateOwners(fm, p, f);
115
+ expect(f.errors).toHaveLength(1);
116
+ expect(f.errors[0]).toContain("invalid owner");
117
+ });
118
+
119
+ it("rejects mixed wildcard", () => {
120
+ const tmp = useTmpDir();
121
+ const root = setup(tmp);
122
+ const p = write(root, "NODE.md", "---\nowners: [alice, *]\n---\n");
123
+ const fm = parseFrontmatter(p)!;
124
+ const f = new Findings();
125
+ validateOwners(fm, p, f);
126
+ expect(f.errors).toHaveLength(1);
127
+ expect(f.errors[0]).toContain("wildcard");
128
+ });
129
+ });
130
+
131
+ // --- validateFolders ---
132
+
133
+ describe("validateFolders", () => {
134
+ it("reports missing NODE.md", () => {
135
+ const tmp = useTmpDir();
136
+ const root = setup(tmp);
137
+ mkdirSync(join(root, "domain"));
138
+ const f = new Findings();
139
+ validateFolders(f);
140
+ expect(f.errors.some((e) => e.includes("missing NODE.md"))).toBe(true);
141
+ });
142
+
143
+ it("passes with valid folder", () => {
144
+ const tmp = useTmpDir();
145
+ const root = setup(tmp);
146
+ mkdirSync(join(root, "domain"));
147
+ writeFileSync(join(root, "domain", "NODE.md"), "---\nowners: [a]\n---\n# D\n");
148
+ const f = new Findings();
149
+ validateFolders(f);
150
+ expect(f.errors).toEqual([]);
151
+ });
152
+ });
153
+
154
+ // --- validateEmptyNodes ---
155
+
156
+ describe("validateEmptyNodes", () => {
157
+ it("flags short body", () => {
158
+ const tmp = useTmpDir();
159
+ const root = setup(tmp);
160
+ const p = write(root, "NODE.md", "---\nowners: [a]\n---\n\n");
161
+ const f = new Findings();
162
+ validateEmptyNodes([p], f);
163
+ expect(f.warnings.some((w) => w.includes("little or no body content"))).toBe(true);
164
+ });
165
+
166
+ it("passes with adequate body", () => {
167
+ const tmp = useTmpDir();
168
+ const root = setup(tmp);
169
+ const body = "This is a meaningful body with enough content to pass the threshold easily.";
170
+ const p = write(root, "NODE.md", `---\nowners: [a]\n---\n${body}\n`);
171
+ const f = new Findings();
172
+ validateEmptyNodes([p], f);
173
+ expect(f.warnings).toEqual([]);
174
+ });
175
+ });
176
+
177
+ // --- validateTitleMismatch ---
178
+
179
+ describe("validateTitleMismatch", () => {
180
+ it("passes with matching title", () => {
181
+ const tmp = useTmpDir();
182
+ const root = setup(tmp);
183
+ const p = write(root, "NODE.md", "---\ntitle: Hello World\nowners: [a]\n---\n# Hello World\n");
184
+ const f = new Findings();
185
+ validateTitleMismatch([p], f);
186
+ expect(f.warnings).toEqual([]);
187
+ });
188
+
189
+ it("flags mismatched title", () => {
190
+ const tmp = useTmpDir();
191
+ const root = setup(tmp);
192
+ const p = write(root, "NODE.md", "---\ntitle: Hello\nowners: [a]\n---\n# Goodbye\n");
193
+ const f = new Findings();
194
+ validateTitleMismatch([p], f);
195
+ expect(f.warnings).toHaveLength(1);
196
+ expect(f.warnings[0]).toContain("differs from");
197
+ });
198
+ });
@@ -0,0 +1,241 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { runInit } from "#skill/engine/init.js";
5
+ import { check, checkProgress, runVerify } from "#skill/engine/verify.js";
6
+ import { Repo } from "#skill/engine/repo.js";
7
+ import {
8
+ AGENT_INSTRUCTIONS_FILE,
9
+ INSTALLED_PROGRESS,
10
+ LEGACY_PROGRESS,
11
+ } from "#skill/engine/runtime/asset-loader.js";
12
+ import {
13
+ useTmpDir,
14
+ makeAgentsMd,
15
+ makeFramework,
16
+ makeLegacyFramework,
17
+ makeNode,
18
+ makeSourceRepo,
19
+ makeMembers,
20
+ makeSourceSkill,
21
+ } from "./helpers.js";
22
+
23
+ // --- check ---
24
+
25
+ describe("check", () => {
26
+ it("returns true on pass", () => {
27
+ expect(check("my check", true)).toBe(true);
28
+ });
29
+
30
+ it("returns false on fail", () => {
31
+ expect(check("my check", false)).toBe(false);
32
+ });
33
+ });
34
+
35
+ // --- checkProgress ---
36
+
37
+ describe("checkProgress", () => {
38
+ it("returns empty for no progress file", () => {
39
+ const tmp = useTmpDir();
40
+ const repo = new Repo(tmp.path);
41
+ expect(checkProgress(repo)).toEqual([]);
42
+ });
43
+
44
+ it("returns empty when all checked", () => {
45
+ const tmp = useTmpDir();
46
+ mkdirSync(join(tmp.path, "skills", "first-tree"), { recursive: true });
47
+ writeFileSync(
48
+ join(tmp.path, INSTALLED_PROGRESS),
49
+ "# Progress\n- [x] Task one\n- [x] Task two\n",
50
+ );
51
+ const repo = new Repo(tmp.path);
52
+ expect(checkProgress(repo)).toEqual([]);
53
+ });
54
+
55
+ it("returns unchecked items", () => {
56
+ const tmp = useTmpDir();
57
+ mkdirSync(join(tmp.path, "skills", "first-tree"), { recursive: true });
58
+ writeFileSync(
59
+ join(tmp.path, INSTALLED_PROGRESS),
60
+ "# Progress\n- [x] Done task\n- [ ] Pending task\n- [ ] Another pending\n",
61
+ );
62
+ const repo = new Repo(tmp.path);
63
+ expect(checkProgress(repo)).toEqual(["Pending task", "Another pending"]);
64
+ });
65
+
66
+ it("returns empty for empty progress", () => {
67
+ const tmp = useTmpDir();
68
+ mkdirSync(join(tmp.path, "skills", "first-tree"), { recursive: true });
69
+ writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "");
70
+ const repo = new Repo(tmp.path);
71
+ expect(checkProgress(repo)).toEqual([]);
72
+ });
73
+
74
+ it("falls back to the legacy progress file", () => {
75
+ const tmp = useTmpDir();
76
+ makeLegacyFramework(tmp.path);
77
+ writeFileSync(
78
+ join(tmp.path, LEGACY_PROGRESS),
79
+ "# Progress\n- [ ] Legacy task\n",
80
+ );
81
+ const repo = new Repo(tmp.path);
82
+ expect(checkProgress(repo)).toEqual(["Legacy task"]);
83
+ });
84
+ });
85
+
86
+ // --- helpers for building a full repo ---
87
+
88
+ function buildFullRepo(root: string): void {
89
+ mkdirSync(join(root, ".git"));
90
+ makeFramework(root);
91
+ writeFileSync(
92
+ join(root, "NODE.md"),
93
+ "---\ntitle: My Org\nowners: [alice]\n---\n# Content\n",
94
+ );
95
+ makeAgentsMd(root, { markers: true });
96
+ makeMembers(root, 1);
97
+ }
98
+
99
+ const passValidator = () => ({ exitCode: 0 });
100
+ const failValidator = () => ({ exitCode: 1 });
101
+
102
+ // --- runVerify — all passing ---
103
+
104
+ describe("runVerify all passing", () => {
105
+ it("returns 0 when all checks pass", () => {
106
+ const tmp = useTmpDir();
107
+ buildFullRepo(tmp.path);
108
+ const repo = new Repo(tmp.path);
109
+ const ret = runVerify(repo, passValidator);
110
+ expect(ret).toBe(0);
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
+ });
180
+ });
181
+
182
+ // --- runVerify — failing checks ---
183
+
184
+ describe("runVerify failing", () => {
185
+ it("fails on empty repo", () => {
186
+ const tmp = useTmpDir();
187
+ const repo = new Repo(tmp.path);
188
+ const ret = runVerify(repo, passValidator);
189
+ expect(ret).toBe(1);
190
+ });
191
+
192
+ it("fails when AGENTS.md is missing", () => {
193
+ const tmp = useTmpDir();
194
+ makeFramework(tmp.path);
195
+ writeFileSync(
196
+ join(tmp.path, "NODE.md"),
197
+ "---\ntitle: My Org\nowners: [alice]\n---\n",
198
+ );
199
+ const repo = new Repo(tmp.path);
200
+ const ret = runVerify(repo, passValidator);
201
+ expect(ret).toBe(1);
202
+ });
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
+
226
+ it("fails when node validation returns non-zero", () => {
227
+ const tmp = useTmpDir();
228
+ buildFullRepo(tmp.path);
229
+ const repo = new Repo(tmp.path);
230
+ const ret = runVerify(repo, failValidator);
231
+ expect(ret).toBe(1);
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
+ });
241
+ });