first-tree 0.0.2 → 0.0.3
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 +73 -39
- package/dist/cli.js +27 -13
- package/dist/help-xEI-s9iN.js +25 -0
- package/dist/init-DtOjj0wc.js +253 -0
- package/dist/installer-rcZpGLnM.js +47 -0
- package/dist/onboarding-6Fr5Gkrk.js +2 -0
- package/dist/onboarding-B9zPGvvG.js +10 -0
- package/dist/repo-BTJG8BU1.js +187 -0
- package/dist/upgrade-COGgI7Rj.js +96 -0
- package/dist/{verify-CSRIkuoM.js → verify-CxN6JiV9.js} +53 -24
- package/package.json +33 -10
- package/skills/first-tree/SKILL.md +109 -0
- package/skills/first-tree/agents/openai.yaml +4 -0
- package/skills/first-tree/assets/framework/VERSION +1 -0
- package/skills/first-tree/assets/framework/examples/claude-code/README.md +14 -0
- package/skills/first-tree/assets/framework/examples/claude-code/settings.json +14 -0
- package/skills/first-tree/assets/framework/helpers/generate-codeowners.ts +224 -0
- package/skills/first-tree/assets/framework/helpers/inject-tree-context.sh +15 -0
- package/skills/first-tree/assets/framework/helpers/run-review.ts +179 -0
- package/skills/first-tree/assets/framework/manifest.json +11 -0
- package/skills/first-tree/assets/framework/prompts/pr-review.md +38 -0
- package/skills/first-tree/assets/framework/templates/agent.md.template +48 -0
- package/skills/first-tree/assets/framework/templates/member-node.md.template +18 -0
- package/skills/first-tree/assets/framework/templates/members-domain.md.template +45 -0
- package/skills/first-tree/assets/framework/templates/root-node.md.template +38 -0
- package/skills/first-tree/assets/framework/workflows/codeowners.yml +31 -0
- package/skills/first-tree/assets/framework/workflows/pr-review.yml +146 -0
- package/skills/first-tree/assets/framework/workflows/validate.yml +19 -0
- package/skills/first-tree/engine/commands/help.ts +32 -0
- package/skills/first-tree/engine/commands/init.ts +1 -0
- package/skills/first-tree/engine/commands/upgrade.ts +1 -0
- package/skills/first-tree/engine/commands/verify.ts +1 -0
- package/skills/first-tree/engine/init.ts +145 -0
- package/skills/first-tree/engine/onboarding.ts +10 -0
- package/skills/first-tree/engine/repo.ts +184 -0
- package/skills/first-tree/engine/rules/agent-instructions.ts +37 -0
- package/skills/first-tree/engine/rules/agent-integration.ts +19 -0
- package/skills/first-tree/engine/rules/ci-validation.ts +72 -0
- package/skills/first-tree/engine/rules/framework.ts +13 -0
- package/skills/first-tree/engine/rules/index.ts +41 -0
- package/skills/first-tree/engine/rules/members.ts +21 -0
- package/skills/first-tree/engine/rules/populate-tree.ts +36 -0
- package/skills/first-tree/engine/rules/root-node.ts +41 -0
- package/skills/first-tree/engine/runtime/adapters.ts +22 -0
- package/skills/first-tree/engine/runtime/asset-loader.ts +134 -0
- package/skills/first-tree/engine/runtime/installer.ts +82 -0
- package/skills/first-tree/engine/runtime/upgrader.ts +23 -0
- package/skills/first-tree/engine/upgrade.ts +176 -0
- package/skills/first-tree/engine/validators/members.ts +215 -0
- package/skills/first-tree/engine/validators/nodes.ts +514 -0
- package/skills/first-tree/engine/verify.ts +97 -0
- package/skills/first-tree/references/about.md +36 -0
- package/skills/first-tree/references/maintainer-architecture.md +59 -0
- package/skills/first-tree/references/maintainer-build-and-distribution.md +56 -0
- package/skills/first-tree/references/maintainer-testing.md +58 -0
- package/skills/first-tree/references/maintainer-thin-cli.md +38 -0
- package/skills/first-tree/references/onboarding.md +162 -0
- package/skills/first-tree/references/ownership-and-naming.md +94 -0
- package/skills/first-tree/references/principles.md +113 -0
- package/skills/first-tree/references/source-map.md +94 -0
- package/skills/first-tree/references/upgrade-contract.md +85 -0
- package/skills/first-tree/scripts/check-skill-sync.sh +133 -0
- package/skills/first-tree/scripts/quick_validate.py +95 -0
- package/skills/first-tree/scripts/run-local-cli.sh +35 -0
- package/skills/first-tree/tests/asset-loader.test.ts +75 -0
- package/skills/first-tree/tests/generate-codeowners.test.ts +94 -0
- package/skills/first-tree/tests/helpers.ts +149 -0
- package/skills/first-tree/tests/init.test.ts +153 -0
- package/skills/first-tree/tests/repo.test.ts +362 -0
- package/skills/first-tree/tests/rules.test.ts +394 -0
- package/skills/first-tree/tests/run-review.test.ts +155 -0
- package/skills/first-tree/tests/skill-artifacts.test.ts +307 -0
- package/skills/first-tree/tests/thin-cli.test.ts +59 -0
- package/skills/first-tree/tests/upgrade.test.ts +89 -0
- package/skills/first-tree/tests/validate-members.test.ts +224 -0
- package/skills/first-tree/tests/validate-nodes.test.ts +198 -0
- package/skills/first-tree/tests/verify.test.ts +142 -0
- package/dist/init-CE_944sb.js +0 -283
- package/dist/repo-BByc3VvM.js +0 -111
- package/dist/upgrade-Chr7z0CY.js +0 -82
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative } from "node:path";
|
|
3
|
+
|
|
4
|
+
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
5
|
+
const VALID_TYPES = new Set(["human", "personal_assistant", "autonomous_agent"]);
|
|
6
|
+
|
|
7
|
+
function rel(path: string, root: string): string {
|
|
8
|
+
return relative(root, path);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseFrontmatter(path: string): string | null {
|
|
12
|
+
try {
|
|
13
|
+
const text = readFileSync(path, "utf-8");
|
|
14
|
+
const m = text.match(FRONTMATTER_RE);
|
|
15
|
+
return m ? m[1] : null;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function extractScalar(fm: string, key: string): string | null {
|
|
22
|
+
const re = new RegExp(`^${key}:\\s*"?([^"\\n]+?)"?\\s*$`, "m");
|
|
23
|
+
const m = fm.match(re);
|
|
24
|
+
return m ? m[1].trim() : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function extractList(fm: string, key: string): string[] | null {
|
|
28
|
+
// Inline: key: [a, b]
|
|
29
|
+
const inlineRe = new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m");
|
|
30
|
+
let m = fm.match(inlineRe);
|
|
31
|
+
if (m) {
|
|
32
|
+
const raw = m[1].trim();
|
|
33
|
+
if (!raw) return [];
|
|
34
|
+
return raw
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Block: key:\n - a\n - b
|
|
41
|
+
const blockRe = new RegExp(`^${key}:\\s*\\n((?:\\s+-\\s+.+\\n?)+)`, "m");
|
|
42
|
+
m = fm.match(blockRe);
|
|
43
|
+
if (m) {
|
|
44
|
+
return m[1]
|
|
45
|
+
.trim()
|
|
46
|
+
.split("\n")
|
|
47
|
+
.filter((line) => line.trim())
|
|
48
|
+
.map((line) =>
|
|
49
|
+
line
|
|
50
|
+
.trim()
|
|
51
|
+
.replace(/^-\s*/, "")
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/^['"]|['"]$/g, ""),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function validateMember(
|
|
61
|
+
nodePath: string,
|
|
62
|
+
treeRoot: string,
|
|
63
|
+
): string[] {
|
|
64
|
+
const errors: string[] = [];
|
|
65
|
+
const loc = rel(nodePath, treeRoot);
|
|
66
|
+
|
|
67
|
+
const fm = parseFrontmatter(nodePath);
|
|
68
|
+
if (fm === null) return [`${loc}: no frontmatter found`];
|
|
69
|
+
|
|
70
|
+
// title
|
|
71
|
+
const title = extractScalar(fm, "title");
|
|
72
|
+
if (!title) errors.push(`${loc}: missing or empty 'title' field`);
|
|
73
|
+
|
|
74
|
+
// owners
|
|
75
|
+
const owners = extractList(fm, "owners");
|
|
76
|
+
if (owners === null) errors.push(`${loc}: missing 'owners' field`);
|
|
77
|
+
|
|
78
|
+
// type
|
|
79
|
+
const memberType = extractScalar(fm, "type");
|
|
80
|
+
if (!memberType) {
|
|
81
|
+
errors.push(`${loc}: missing 'type' field`);
|
|
82
|
+
} else if (!VALID_TYPES.has(memberType)) {
|
|
83
|
+
errors.push(
|
|
84
|
+
`${loc}: invalid type '${memberType}' — must be one of: ${[...VALID_TYPES].sort().join(", ")}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// role
|
|
89
|
+
const role = extractScalar(fm, "role");
|
|
90
|
+
if (!role) errors.push(`${loc}: missing or empty 'role' field`);
|
|
91
|
+
|
|
92
|
+
// domains
|
|
93
|
+
const domains = extractList(fm, "domains");
|
|
94
|
+
if (domains === null) {
|
|
95
|
+
errors.push(`${loc}: missing 'domains' field`);
|
|
96
|
+
} else if (domains.length === 0) {
|
|
97
|
+
errors.push(`${loc}: 'domains' must contain at least one entry`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return errors;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Info collected per member for cross-validation. */
|
|
104
|
+
type MemberInfo = {
|
|
105
|
+
name: string;
|
|
106
|
+
relPath: string;
|
|
107
|
+
type: string | null;
|
|
108
|
+
delegateMention: string | null;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export function runValidateMembers(treeRoot: string): {
|
|
112
|
+
exitCode: number;
|
|
113
|
+
errors: string[];
|
|
114
|
+
} {
|
|
115
|
+
const membersDir = join(treeRoot, "members");
|
|
116
|
+
if (!existsSync(membersDir) || !statSync(membersDir).isDirectory()) {
|
|
117
|
+
console.log(`Members directory not found: ${membersDir}`);
|
|
118
|
+
return { exitCode: 1, errors: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allErrors: string[] = [];
|
|
122
|
+
let memberCount = 0;
|
|
123
|
+
|
|
124
|
+
// Collected for cross-validation (name uniqueness + delegate_mention)
|
|
125
|
+
const nameOccurrences = new Map<string, string[]>();
|
|
126
|
+
const members: MemberInfo[] = [];
|
|
127
|
+
|
|
128
|
+
function walk(dir: string): void {
|
|
129
|
+
for (const child of readdirSync(dir).sort()) {
|
|
130
|
+
const childPath = join(dir, child);
|
|
131
|
+
|
|
132
|
+
// Reject stray .md files
|
|
133
|
+
try {
|
|
134
|
+
const stat = statSync(childPath);
|
|
135
|
+
if (stat.isFile() && child.endsWith(".md") && child !== "NODE.md") {
|
|
136
|
+
allErrors.push(
|
|
137
|
+
`${rel(childPath, treeRoot)}: member must be a directory with NODE.md, not a standalone file — use ${rel(dir, treeRoot)}/${child.replace(/\.md$/, "")}/NODE.md instead`,
|
|
138
|
+
);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!stat.isDirectory()) continue;
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Track directory name occurrences for uniqueness check
|
|
147
|
+
const relPath = relative(membersDir, childPath);
|
|
148
|
+
const occurrences = nameOccurrences.get(child) ?? [];
|
|
149
|
+
occurrences.push(relPath);
|
|
150
|
+
nameOccurrences.set(child, occurrences);
|
|
151
|
+
|
|
152
|
+
const nodePath = join(childPath, "NODE.md");
|
|
153
|
+
if (!existsSync(nodePath)) {
|
|
154
|
+
allErrors.push(`${rel(childPath, treeRoot)}/: directory exists but missing NODE.md`);
|
|
155
|
+
walk(childPath);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
memberCount++;
|
|
160
|
+
allErrors.push(...validateMember(nodePath, treeRoot));
|
|
161
|
+
|
|
162
|
+
// Collect info for cross-validation
|
|
163
|
+
const fm = parseFrontmatter(nodePath);
|
|
164
|
+
if (fm) {
|
|
165
|
+
members.push({
|
|
166
|
+
name: child,
|
|
167
|
+
relPath,
|
|
168
|
+
type: extractScalar(fm, "type"),
|
|
169
|
+
delegateMention: extractScalar(fm, "delegate_mention"),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Recurse into subdirectories
|
|
174
|
+
walk(childPath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
walk(membersDir);
|
|
179
|
+
|
|
180
|
+
// Cross-validation: directory name uniqueness
|
|
181
|
+
for (const [name, paths] of nameOccurrences) {
|
|
182
|
+
if (paths.length > 1) {
|
|
183
|
+
allErrors.push(
|
|
184
|
+
`Duplicate member directory name '${name}' found at: ${paths.map((p) => `members/${p}`).join(", ")} — directory names must be unique across all levels under members/`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Cross-validation: delegate_mention references
|
|
190
|
+
const memberByName = new Map(members.map((m) => [m.name, m]));
|
|
191
|
+
for (const member of members) {
|
|
192
|
+
if (!member.delegateMention) continue;
|
|
193
|
+
const target = memberByName.get(member.delegateMention);
|
|
194
|
+
if (!target) {
|
|
195
|
+
allErrors.push(
|
|
196
|
+
`members/${member.relPath}/NODE.md: delegate_mention '${member.delegateMention}' references non-existent member — the target must be a directory under members/`,
|
|
197
|
+
);
|
|
198
|
+
} else if (target.type !== "personal_assistant") {
|
|
199
|
+
allErrors.push(
|
|
200
|
+
`members/${member.relPath}/NODE.md: delegate_mention '${member.delegateMention}' must reference a member with type 'personal_assistant', but '${target.name}' has type '${target.type}'`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (allErrors.length > 0) {
|
|
206
|
+
console.log(`Found ${allErrors.length} member validation error(s):\n`);
|
|
207
|
+
for (const err of allErrors) {
|
|
208
|
+
console.log(` \u2717 ${err}`);
|
|
209
|
+
}
|
|
210
|
+
return { exitCode: 1, errors: allErrors };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`All ${memberCount} member(s) passed validation.`);
|
|
214
|
+
return { exitCode: 0, errors: allErrors };
|
|
215
|
+
}
|