first-tree 0.0.1 → 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.
Files changed (80) hide show
  1. package/README.md +73 -39
  2. package/dist/cli.js +27 -13
  3. package/dist/help-xEI-s9iN.js +25 -0
  4. package/dist/init-DtOjj0wc.js +253 -0
  5. package/dist/installer-rcZpGLnM.js +47 -0
  6. package/dist/onboarding-6Fr5Gkrk.js +2 -0
  7. package/dist/onboarding-B9zPGvvG.js +10 -0
  8. package/dist/repo-BTJG8BU1.js +187 -0
  9. package/dist/upgrade-COGgI7Rj.js +96 -0
  10. package/dist/{verify-DIz6qmBX.js → verify-CxN6JiV9.js} +135 -8
  11. package/package.json +33 -10
  12. package/skills/first-tree/SKILL.md +109 -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 +179 -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/agent.md.template +48 -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 +38 -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 +145 -0
  34. package/skills/first-tree/engine/onboarding.ts +10 -0
  35. package/skills/first-tree/engine/repo.ts +184 -0
  36. package/skills/first-tree/engine/rules/agent-instructions.ts +37 -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 +134 -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 +176 -0
  49. package/skills/first-tree/engine/validators/members.ts +215 -0
  50. package/skills/first-tree/engine/validators/nodes.ts +514 -0
  51. package/skills/first-tree/engine/verify.ts +97 -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 +56 -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 +162 -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 +85 -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 +149 -0
  68. package/skills/first-tree/tests/init.test.ts +153 -0
  69. package/skills/first-tree/tests/repo.test.ts +362 -0
  70. package/skills/first-tree/tests/rules.test.ts +394 -0
  71. package/skills/first-tree/tests/run-review.test.ts +155 -0
  72. package/skills/first-tree/tests/skill-artifacts.test.ts +307 -0
  73. package/skills/first-tree/tests/thin-cli.test.ts +59 -0
  74. package/skills/first-tree/tests/upgrade.test.ts +89 -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 +142 -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,514 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join, relative, posix } from "node:path";
3
+
4
+ const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
5
+ const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
6
+ const SOFT_LINKS_INLINE_RE = /^soft_links:\s*\[([^\]]*)\]/m;
7
+ const SOFT_LINKS_BLOCK_RE = /^soft_links:\s*\n((?:\s+-\s+.+\n?)+)/m;
8
+ const TITLE_RE = /^title:\s*['"]?(.+?)['"]?\s*$/m;
9
+ const GITHUB_USER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/;
10
+ const MD_LINK_RE = /\[.*?\]\(([^)]+\.md)\)/g;
11
+ const DOMAIN_LINK_RE = /\[(\w[\w-]*)\/?\]\((\w[\w-]*)\/NODE\.md\)/g;
12
+
13
+ const SKIP = new Set(["node_modules", "__pycache__"]);
14
+ const SKIP_FILES = new Set(["AGENT.md", "CLAUDE.md"]);
15
+ const MIN_BODY_LENGTH = 20;
16
+
17
+ export class Findings {
18
+ errors: string[] = [];
19
+ warnings: string[] = [];
20
+ infos: string[] = [];
21
+
22
+ error(msg: string): void {
23
+ this.errors.push(msg);
24
+ }
25
+ warning(msg: string): void {
26
+ this.warnings.push(msg);
27
+ }
28
+ info(msg: string): void {
29
+ this.infos.push(msg);
30
+ }
31
+ hasErrors(): boolean {
32
+ return this.errors.length > 0;
33
+ }
34
+ printReport(totalFiles: number): void {
35
+ const all: [string, string][] = [
36
+ ...this.errors.map((e): [string, string] => ["error", e]),
37
+ ...this.warnings.map((w): [string, string] => ["warning", w]),
38
+ ...this.infos.map((i): [string, string] => ["info", i]),
39
+ ];
40
+ if (all.length > 0) {
41
+ const counts: string[] = [];
42
+ if (this.errors.length) counts.push(`${this.errors.length} error(s)`);
43
+ if (this.warnings.length) counts.push(`${this.warnings.length} warning(s)`);
44
+ if (this.infos.length) counts.push(`${this.infos.length} info(s)`);
45
+ console.log(`Found ${counts.join(", ")}:\n`);
46
+ const icons: Record<string, string> = {
47
+ error: "\u2717",
48
+ warning: "\u26a0",
49
+ info: "\u2139",
50
+ };
51
+ for (const [severity, msg] of all) {
52
+ console.log(` ${icons[severity]} [${severity}] ${msg}`);
53
+ }
54
+ } else {
55
+ console.log(`All ${totalFiles} node(s) passed validation.`);
56
+ }
57
+ }
58
+ }
59
+
60
+ // -- Utilities --
61
+
62
+ let treeRoot = "";
63
+ const textCache = new Map<string, string | null>();
64
+
65
+ export function setTreeRoot(root: string): void {
66
+ treeRoot = root;
67
+ textCache.clear();
68
+ }
69
+
70
+ export function getTreeRoot(): string {
71
+ return treeRoot;
72
+ }
73
+
74
+ function rel(path: string): string {
75
+ return relative(treeRoot, path);
76
+ }
77
+
78
+ function shouldSkip(path: string): boolean {
79
+ const parts = relative(treeRoot, path).split("/");
80
+ return parts.some((part) => SKIP.has(part) || part.startsWith("."));
81
+ }
82
+
83
+ function readText(path: string): string | null {
84
+ if (!textCache.has(path)) {
85
+ try {
86
+ textCache.set(path, readFileSync(path, "utf-8"));
87
+ } catch {
88
+ textCache.set(path, null);
89
+ }
90
+ }
91
+ return textCache.get(path)!;
92
+ }
93
+
94
+ export function parseFrontmatter(path: string): string | null {
95
+ const text = readText(path);
96
+ if (text === null) return null;
97
+ const m = text.match(FRONTMATTER_RE);
98
+ return m ? m[1] : null;
99
+ }
100
+
101
+ export function parseBody(path: string): string | null {
102
+ const text = readText(path);
103
+ if (text === null) return null;
104
+ const m = text.match(FRONTMATTER_RE);
105
+ if (m) return text.slice(m[0].length);
106
+ return text;
107
+ }
108
+
109
+ export function parseSoftLinks(fm: string): string[] | null {
110
+ // Inline format
111
+ let m = fm.match(SOFT_LINKS_INLINE_RE);
112
+ if (m) {
113
+ const raw = m[1].trim();
114
+ if (!raw) return [];
115
+ return raw
116
+ .split(",")
117
+ .map((s) => s.trim().replace(/^['"]|['"]$/g, ""))
118
+ .filter(Boolean);
119
+ }
120
+ // Block format
121
+ m = fm.match(SOFT_LINKS_BLOCK_RE);
122
+ if (m) {
123
+ return m[1]
124
+ .trim()
125
+ .split("\n")
126
+ .map((line) =>
127
+ line
128
+ .trim()
129
+ .replace(/^-\s*/, "")
130
+ .trim()
131
+ .replace(/^['"]|['"]$/g, ""),
132
+ );
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function resolveSoftLink(link: string): boolean {
138
+ const clean = link.replace(/^\/+/, "");
139
+ const target = join(treeRoot, clean);
140
+
141
+ // Direct .md file
142
+ try {
143
+ if (statSync(target).isFile() && target.endsWith(".md")) return true;
144
+ } catch {
145
+ // not found
146
+ }
147
+ // Directory with NODE.md
148
+ try {
149
+ if (statSync(target).isDirectory() && existsSync(join(target, "NODE.md")))
150
+ return true;
151
+ } catch {
152
+ // not found
153
+ }
154
+ return false;
155
+ }
156
+
157
+ function normalizeSoftLink(link: string): string {
158
+ const clean = link.replace(/^\/+/, "");
159
+ const target = join(treeRoot, clean);
160
+ try {
161
+ if (statSync(target).isDirectory()) return join(target, "NODE.md");
162
+ } catch {
163
+ // not a directory
164
+ }
165
+ return target;
166
+ }
167
+
168
+ export function collectMdFiles(): string[] {
169
+ const files: string[] = [];
170
+ function walk(dir: string): void {
171
+ let entries: string[];
172
+ try {
173
+ entries = readdirSync(dir).sort();
174
+ } catch {
175
+ return;
176
+ }
177
+ for (const entry of entries) {
178
+ const full = join(dir, entry);
179
+ if (shouldSkip(full)) continue;
180
+ try {
181
+ const stat = statSync(full);
182
+ if (stat.isDirectory()) {
183
+ walk(full);
184
+ } else if (
185
+ stat.isFile() &&
186
+ entry.endsWith(".md") &&
187
+ !SKIP_FILES.has(entry)
188
+ ) {
189
+ files.push(full);
190
+ }
191
+ } catch {
192
+ // skip
193
+ }
194
+ }
195
+ }
196
+ walk(treeRoot);
197
+ return files;
198
+ }
199
+
200
+ // -- Validation checks --
201
+
202
+ export function validateOwners(
203
+ fm: string,
204
+ path: string,
205
+ findings: Findings,
206
+ ): void {
207
+ const m = fm.match(OWNERS_RE);
208
+ if (!m) {
209
+ findings.error(`${rel(path)}: missing 'owners' field in frontmatter`);
210
+ return;
211
+ }
212
+ const raw = m[1].trim();
213
+ if (!raw) return; // owners: [] is valid (inheritance)
214
+
215
+ const owners = raw
216
+ .split(",")
217
+ .map((o) => o.trim())
218
+ .filter(Boolean);
219
+ if (owners.length === 0) {
220
+ findings.error(`${rel(path)}: owners list contains only whitespace entries`);
221
+ return;
222
+ }
223
+ if (owners.length === 1 && owners[0] === "*") return; // owners: [*] valid
224
+
225
+ for (const owner of owners) {
226
+ if (owner === "*") {
227
+ findings.error(
228
+ `${rel(path)}: wildcard '*' must be the sole entry, not mixed with usernames`,
229
+ );
230
+ } else if (!GITHUB_USER_RE.test(owner)) {
231
+ findings.error(`${rel(path)}: invalid owner '${owner}'`);
232
+ }
233
+ }
234
+ }
235
+
236
+ export function validateSoftLinks(
237
+ fm: string,
238
+ path: string,
239
+ findings: Findings,
240
+ ): void {
241
+ const links = parseSoftLinks(fm);
242
+ if (links === null) return;
243
+ for (const link of links) {
244
+ if (!link) {
245
+ findings.error(`${rel(path)}: empty soft_link entry`);
246
+ } else if (!resolveSoftLink(link)) {
247
+ findings.error(
248
+ `${rel(path)}: soft_link '${link}' does not resolve to an existing node`,
249
+ );
250
+ }
251
+ }
252
+ }
253
+
254
+ export function validateFolders(findings: Findings): void {
255
+ function walk(dir: string): void {
256
+ let entries: string[];
257
+ try {
258
+ entries = readdirSync(dir).sort();
259
+ } catch {
260
+ return;
261
+ }
262
+ for (const entry of entries) {
263
+ const full = join(dir, entry);
264
+ if (shouldSkip(full)) continue;
265
+ try {
266
+ if (!statSync(full).isDirectory()) continue;
267
+ } catch {
268
+ continue;
269
+ }
270
+ if (!existsSync(join(full, "NODE.md"))) {
271
+ findings.error(`${rel(full)}/: missing NODE.md`);
272
+ }
273
+ walk(full);
274
+ }
275
+ }
276
+ walk(treeRoot);
277
+ }
278
+
279
+ export function validateDirectoryListing(findings: Findings): void {
280
+ function walk(dir: string): void {
281
+ let entries: string[];
282
+ try {
283
+ entries = readdirSync(dir).sort();
284
+ } catch {
285
+ return;
286
+ }
287
+ for (const entry of entries) {
288
+ const full = join(dir, entry);
289
+ if (shouldSkip(full)) continue;
290
+ try {
291
+ if (!statSync(full).isDirectory()) continue;
292
+ } catch {
293
+ continue;
294
+ }
295
+ const nodeMd = join(full, "NODE.md");
296
+ if (!existsSync(nodeMd)) {
297
+ walk(full);
298
+ continue;
299
+ }
300
+ const body = parseBody(nodeMd);
301
+ if (body !== null) {
302
+ const actualLeaves = new Set<string>();
303
+ for (const f of readdirSync(full).sort()) {
304
+ const fp = join(full, f);
305
+ try {
306
+ if (statSync(fp).isFile() && f.endsWith(".md") && f !== "NODE.md") {
307
+ actualLeaves.add(f);
308
+ }
309
+ } catch {
310
+ // skip
311
+ }
312
+ }
313
+ const referenced = new Set<string>();
314
+ let linkMatch: RegExpExecArray | null;
315
+ const linkRe = /\[.*?\]\(([^)]+\.md)\)/g;
316
+ while ((linkMatch = linkRe.exec(body)) !== null) {
317
+ const ref = linkMatch[1];
318
+ if (ref.startsWith("http") || ref.startsWith("/")) continue;
319
+ if (!ref.includes("/")) referenced.add(ref);
320
+ }
321
+ for (const orphan of [...actualLeaves].filter((f) => !referenced.has(f)).sort()) {
322
+ findings.warning(
323
+ `${rel(nodeMd)}: leaf file '${orphan}' exists but is not mentioned in NODE.md`,
324
+ );
325
+ }
326
+ for (const ref of [...referenced].filter((f) => !actualLeaves.has(f)).sort()) {
327
+ if (!existsSync(join(full, ref))) {
328
+ findings.warning(
329
+ `${rel(nodeMd)}: references '${ref}' but the file does not exist`,
330
+ );
331
+ }
332
+ }
333
+ }
334
+ walk(full);
335
+ }
336
+ }
337
+ walk(treeRoot);
338
+ }
339
+
340
+ export function validateRootDomainSync(findings: Findings): void {
341
+ const nodeMd = join(treeRoot, "NODE.md");
342
+ const body = parseBody(nodeMd);
343
+ if (body === null) return;
344
+
345
+ // Strip HTML comments
346
+ const bodyNoComments = body.replace(/<!--.*?-->/gs, "");
347
+
348
+ const listedDomains = new Set<string>();
349
+ let dm: RegExpExecArray | null;
350
+ const domainRe = /\[(\w[\w-]*)\/?\]\((\w[\w-]*)\/NODE\.md\)/g;
351
+ while ((dm = domainRe.exec(bodyNoComments)) !== null) {
352
+ listedDomains.add(dm[2]);
353
+ }
354
+
355
+ const actualDomains = new Set<string>();
356
+ for (const child of readdirSync(treeRoot).sort()) {
357
+ const full = join(treeRoot, child);
358
+ try {
359
+ if (!statSync(full).isDirectory()) continue;
360
+ } catch {
361
+ continue;
362
+ }
363
+ if (child.startsWith(".") || SKIP.has(child)) continue;
364
+ if (existsSync(join(full, "NODE.md"))) actualDomains.add(child);
365
+ }
366
+
367
+ for (const missing of [...actualDomains].filter((d) => !listedDomains.has(d)).sort()) {
368
+ findings.error(
369
+ `NODE.md: domain directory '${missing}/' exists but is not listed in root NODE.md`,
370
+ );
371
+ }
372
+ for (const extra of [...listedDomains].filter((d) => !actualDomains.has(d)).sort()) {
373
+ findings.error(
374
+ `NODE.md: lists domain '${extra}/' but the directory does not exist or has no NODE.md`,
375
+ );
376
+ }
377
+ }
378
+
379
+ export function validateSoftLinkReciprocity(
380
+ files: string[],
381
+ findings: Findings,
382
+ ): void {
383
+ const allLinks: [string, string][] = [];
384
+
385
+ for (const path of files) {
386
+ const fm = parseFrontmatter(path);
387
+ if (fm === null) continue;
388
+ const links = parseSoftLinks(fm);
389
+ if (!links) continue;
390
+ for (const link of links) {
391
+ if (!link) continue;
392
+ const target = normalizeSoftLink(link);
393
+ allLinks.push([path, target]);
394
+ }
395
+ }
396
+
397
+ for (const [source, target] of allLinks) {
398
+ if (!existsSync(target)) continue;
399
+
400
+ let hasBackLink = false;
401
+ const targetFm = parseFrontmatter(target);
402
+ if (targetFm) {
403
+ const targetLinks = parseSoftLinks(targetFm);
404
+ if (targetLinks) {
405
+ for (const tl of targetLinks) {
406
+ if (!tl) continue;
407
+ const resolved = normalizeSoftLink(tl);
408
+ if (
409
+ resolved === source ||
410
+ resolved === join(source, "..", "NODE.md")
411
+ ) {
412
+ hasBackLink = true;
413
+ break;
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ if (!hasBackLink) {
420
+ const targetBody = parseBody(target);
421
+ if (targetBody) {
422
+ const sourceRel = rel(source);
423
+ const linkRe = /\[.*?\]\(([^)]+\.md)\)/g;
424
+ let lm: RegExpExecArray | null;
425
+ while ((lm = linkRe.exec(targetBody)) !== null) {
426
+ const ref = lm[1];
427
+ if (ref.startsWith("http") || ref.startsWith("/")) continue;
428
+ if (sourceRel.endsWith(ref) || ref === sourceRel) {
429
+ hasBackLink = true;
430
+ break;
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ if (!hasBackLink) {
437
+ findings.info(
438
+ `${rel(source)}: soft_link to '${rel(target)}' is one-way (target has no reference back)`,
439
+ );
440
+ }
441
+ }
442
+ }
443
+
444
+ export function validateEmptyNodes(
445
+ files: string[],
446
+ findings: Findings,
447
+ ): void {
448
+ for (const path of files) {
449
+ const text = readText(path);
450
+ if (text === null) continue;
451
+ const m = text.match(FRONTMATTER_RE);
452
+ if (!m) continue;
453
+ const body = text.slice(m[0].length);
454
+ const stripped = body.replace(/\s+/g, "");
455
+ if (stripped.length < MIN_BODY_LENGTH) {
456
+ findings.warning(`${rel(path)}: node has little or no body content`);
457
+ }
458
+ }
459
+ }
460
+
461
+ export function validateTitleMismatch(
462
+ files: string[],
463
+ findings: Findings,
464
+ ): void {
465
+ for (const path of files) {
466
+ const text = readText(path);
467
+ if (text === null) continue;
468
+ const fmMatch = text.match(FRONTMATTER_RE);
469
+ if (!fmMatch) continue;
470
+
471
+ const fm = fmMatch[1];
472
+ const titleMatch = fm.match(TITLE_RE);
473
+ if (!titleMatch) continue;
474
+ const fmTitle = titleMatch[1].trim();
475
+
476
+ const body = text.slice(fmMatch[0].length);
477
+ const headingMatch = body.match(/^#\s+(.+)$/m);
478
+ if (!headingMatch) continue;
479
+ const bodyHeading = headingMatch[1].trim();
480
+
481
+ if (fmTitle !== bodyHeading) {
482
+ findings.warning(
483
+ `${rel(path)}: frontmatter title '${fmTitle}' differs from first heading '${bodyHeading}'`,
484
+ );
485
+ }
486
+ }
487
+ }
488
+
489
+ export function runValidateNodes(root: string): { exitCode: number; findings: Findings } {
490
+ setTreeRoot(root);
491
+ const files = collectMdFiles();
492
+ const findings = new Findings();
493
+
494
+ validateFolders(findings);
495
+
496
+ for (const path of files) {
497
+ const fm = parseFrontmatter(path);
498
+ if (fm === null) {
499
+ findings.error(`${rel(path)}: no frontmatter found`);
500
+ continue;
501
+ }
502
+ validateOwners(fm, path, findings);
503
+ validateSoftLinks(fm, path, findings);
504
+ }
505
+
506
+ validateDirectoryListing(findings);
507
+ validateRootDomainSync(findings);
508
+ validateSoftLinkReciprocity(files, findings);
509
+ validateEmptyNodes(files, findings);
510
+ validateTitleMismatch(files, findings);
511
+
512
+ findings.printReport(files.length);
513
+ return { exitCode: findings.hasErrors() ? 1 : 0, findings };
514
+ }
@@ -0,0 +1,97 @@
1
+ import { Repo } from "#skill/engine/repo.js";
2
+ import { runValidateMembers } from "#skill/engine/validators/members.js";
3
+ import { runValidateNodes } from "#skill/engine/validators/nodes.js";
4
+
5
+ const UNCHECKED_RE = /^- \[ \] (.+)$/gm;
6
+
7
+ export function check(label: string, passed: boolean): boolean {
8
+ const icon = passed ? "\u2713" : "\u2717";
9
+ const status = passed ? "PASS" : "FAIL";
10
+ console.log(` ${icon} [${status}] ${label}`);
11
+ return passed;
12
+ }
13
+
14
+ export function checkProgress(repo: Repo): string[] {
15
+ const progressPath = repo.progressPath();
16
+ const text = progressPath === null ? null : repo.readFile(progressPath);
17
+ if (text === null) return [];
18
+ const matches: string[] = [];
19
+ let m: RegExpExecArray | null;
20
+ UNCHECKED_RE.lastIndex = 0;
21
+ while ((m = UNCHECKED_RE.exec(text)) !== null) {
22
+ matches.push(m[1]);
23
+ }
24
+ return matches;
25
+ }
26
+
27
+ export interface ValidateNodesResult {
28
+ exitCode: number;
29
+ }
30
+
31
+ export type NodeValidator = (root: string) => ValidateNodesResult;
32
+
33
+ function defaultNodeValidator(root: string): ValidateNodesResult {
34
+ const { exitCode } = runValidateNodes(root);
35
+ return { exitCode };
36
+ }
37
+
38
+ export function runVerify(repo?: Repo, nodeValidator?: NodeValidator): number {
39
+ const r = repo ?? new Repo();
40
+ const validate = nodeValidator ?? defaultNodeValidator;
41
+ let allPassed = true;
42
+ const progressPath = r.progressPath() ?? r.preferredProgressPath();
43
+ const frameworkVersionPath = r.frameworkVersionPath();
44
+
45
+ console.log("Context Tree Verification\n");
46
+
47
+ // Progress file check
48
+ const unchecked = checkProgress(r);
49
+ if (unchecked.length > 0) {
50
+ console.log(` Unchecked items in ${progressPath}:\n`);
51
+ for (const item of unchecked) {
52
+ console.log(` - [ ] ${item}`);
53
+ }
54
+ console.log();
55
+ console.log(
56
+ ` Verify each step above and check it off in ${progressPath} before running verify again.\n`,
57
+ );
58
+ allPassed = false;
59
+ }
60
+
61
+ // Deterministic checks
62
+ console.log(" Checks:\n");
63
+
64
+ // 1. Framework exists
65
+ allPassed = check(`${frameworkVersionPath} exists`, r.hasFramework()) && allPassed;
66
+
67
+ // 2. Root NODE.md has valid frontmatter
68
+ const fm = r.frontmatter("NODE.md");
69
+ const hasValidNode =
70
+ fm !== null && fm.title !== undefined && fm.owners !== undefined;
71
+ allPassed = check(
72
+ "Root NODE.md has valid frontmatter (title, owners)",
73
+ hasValidNode,
74
+ ) && allPassed;
75
+
76
+ // 3. AGENT.md exists with framework markers
77
+ allPassed = check(
78
+ "AGENT.md exists with framework markers",
79
+ r.hasAgentMdMarkers(),
80
+ ) && allPassed;
81
+
82
+ // 4. Node validation
83
+ const { exitCode } = validate(r.root);
84
+ allPassed = check("Node validation passes", exitCode === 0) && allPassed;
85
+
86
+ // 5. Member validation
87
+ const members = runValidateMembers(r.root);
88
+ allPassed = check("Member validation passes", members.exitCode === 0) && allPassed;
89
+
90
+ console.log();
91
+ if (allPassed) {
92
+ console.log("All checks passed.");
93
+ } else {
94
+ console.log("Some checks failed. See above for details.");
95
+ }
96
+ return allPassed ? 0 : 1;
97
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: "About Context Tree"
3
+ owners: []
4
+ ---
5
+
6
+ # About Context Tree
7
+
8
+ **context-tree.ai** — The living source of truth for your organization.
9
+
10
+ ---
11
+
12
+ ## The Problem
13
+
14
+ Organizations generate decisions constantly — in PRs, meetings, Slack threads, documents. But none of these systems stay current. PRs get merged, issues get closed, documents decay. The knowledge that produced them scatters and disappears.
15
+
16
+ When an agent — or a new teammate — needs to understand *why* something was built a certain way, there's nowhere to look. The information existed once, but no system kept it alive.
17
+
18
+ ---
19
+
20
+ ## The Idea
21
+
22
+ Context Tree is the **living source of truth** for an organization — a tree-structured knowledge base that agents and humans build and maintain together.
23
+
24
+ Every node represents a domain, decision, or design. Every node has an **owner**. When a decision is made, it is written to the tree. When things change, the tree updates. The tree is never a snapshot — it's the current state.
25
+
26
+ The result is an organization where:
27
+
28
+ - Every agent and every human reads from the same, always-current source
29
+ - Decisions are traceable — the *what*, the *why*, and who owns it
30
+ - Knowledge compounds over time instead of evaporating
31
+
32
+ ---
33
+
34
+ ## Who It's For
35
+
36
+ Agent-centric teams — founders, engineers, and product builders who work alongside AI agents every day and want their organizational knowledge to grow with them, not against them.
@@ -0,0 +1,59 @@
1
+ # Maintainer Architecture
2
+
3
+ This reference explains how to maintain the `first-tree` source repo itself.
4
+
5
+ ## What This Repo Ships
6
+
7
+ - One canonical skill: `skills/first-tree/`
8
+ - One thin CLI package: the `context-tree` command distributed by the `first-tree`
9
+ npm package
10
+ - The published package carries that canonical skill directly; normal install
11
+ and upgrade flows should not depend on cloning this source repo
12
+
13
+ This repo is not a user context tree. User decision content lives in the repos
14
+ that install the framework.
15
+
16
+ ## Canonical Layers
17
+
18
+ 1. `SKILL.md` defines when to use the skill and the maintainer workflow.
19
+ 2. `references/` stores the knowledge an agent needs to maintain the framework
20
+ and the thin CLI without reading repo-local prose.
21
+ 3. `assets/framework/` stores the runtime payload that gets installed into user
22
+ repos.
23
+ 4. `engine/` stores the canonical framework and CLI behavior.
24
+ 5. `tests/` store the canonical skill validation surface.
25
+ 6. The root repo may also keep maintainer-only developer tooling such as
26
+ `evals/` when that tooling should not ship with the skill.
27
+ 7. The root CLI/package files are implementation shell code. They should call
28
+ into the skill-owned engine and validation surface, not become a second
29
+ source of framework knowledge.
30
+
31
+ ## Non-Negotiables
32
+
33
+ - Treat `skills/first-tree/` as the only canonical source.
34
+ - If a maintainer needs information to safely change behavior, move that
35
+ information into `references/`; do not leave it only in root `README.md`,
36
+ `AGENT.md`, CI comments, or PR descriptions.
37
+ - Keep runtime assets generic. They are copied into every user tree.
38
+ - Keep the CLI thin. Command semantics, upgrade rules, layout contracts, and
39
+ maintainer guidance should belong to the skill.
40
+ - Keep the user tree decision-focused. Execution detail stays in source systems.
41
+
42
+ ## Change Discipline
43
+
44
+ - Path or layout changes: update `references/upgrade-contract.md`, task text,
45
+ validators, and tests together.
46
+ - Shipped payload changes: update `assets/framework/`, the maintainer references
47
+ that describe the contract, and the validation surface together.
48
+ - Thin shell changes: update the relevant maintainer reference before or during
49
+ the code change so the skill remains self-sufficient.
50
+
51
+ ## End-State Target
52
+
53
+ - skill owns knowledge, runtime payload, framework engine, and the canonical
54
+ framework test surface
55
+ - root owns only the light CLI/bootstrap/build shell plus maintainer-only
56
+ developer tooling such as `evals/`
57
+
58
+ When deciding where a new file should live, bias toward the skill unless the
59
+ file is purely package-tooling shell code.