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.
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-CSRIkuoM.js → verify-CxN6JiV9.js} +53 -24
  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,85 @@
1
+ # Upgrade Contract
2
+
3
+ This file describes the current installed-layout contract and the compatibility
4
+ rules we keep for legacy `skills/first-tree-cli-framework/` and
5
+ `.context-tree/` repos.
6
+
7
+ ## Canonical Source
8
+
9
+ - `skills/first-tree/` is the only source of truth.
10
+ - `references/` contains explanatory material.
11
+ - `assets/framework/` contains the shipped runtime payload.
12
+ - The distributable `first-tree` package must carry the canonical skill inside
13
+ the package itself.
14
+ - The source repo does not keep a root `.context-tree/`, `docs/`, mirror skill
15
+ directories, or a bundled repo snapshot.
16
+
17
+ ## Installed Layout
18
+
19
+ The current installed layout in a user repo is:
20
+
21
+ ```text
22
+ skills/
23
+ first-tree/
24
+ SKILL.md
25
+ progress.md
26
+ references/
27
+ assets/
28
+ framework/
29
+ manifest.json
30
+ VERSION
31
+ templates/
32
+ workflows/
33
+ prompts/
34
+ examples/
35
+ helpers/
36
+ ```
37
+
38
+ The tree content still lives outside the skill:
39
+
40
+ - `NODE.md`
41
+ - `AGENT.md`
42
+ - `members/`
43
+
44
+ ## Command Intent
45
+
46
+ - `context-tree init`
47
+ - installs the skill into the target repo
48
+ - renders top-level tree scaffolding from the skill templates
49
+ - writes progress state to `skills/first-tree/progress.md`
50
+ - `context-tree verify`
51
+ - checks progress state from the installed skill
52
+ - validates root/frontmatter/agent markers
53
+ - runs node and member validators
54
+ - `context-tree upgrade`
55
+ - compares the installed skill payload version to the skill bundled with the
56
+ currently running `first-tree` package
57
+ - refreshes the installed skill payload without overwriting tree content
58
+ - migrates repos that still use the previous
59
+ `skills/first-tree-cli-framework/` path onto `skills/first-tree/`
60
+ - migrates legacy `.context-tree/` repos onto the installed skill layout
61
+ - preserves user-authored sections such as the editable part of `AGENT.md`
62
+
63
+ ## Compatibility Rules For Legacy Trees
64
+
65
+ - `context-tree init` only installs the skill layout; it never creates a new
66
+ `.context-tree/`.
67
+ - Normal `context-tree init` and `context-tree upgrade` flows do not clone the
68
+ source repo or require network access.
69
+ - `context-tree verify` may still read a legacy
70
+ `skills/first-tree-cli-framework/...` or `.context-tree/...` layout in an
71
+ existing user repo so the repo can be upgraded in place.
72
+ - `context-tree upgrade` must migrate either legacy layout onto
73
+ `skills/first-tree/` and remove the old directory afterward.
74
+ - When both layouts are present, prefer the installed skill layout.
75
+
76
+ ## Invariants
77
+
78
+ - Templates, workflows, prompts, helper scripts, and explanatory references
79
+ must stay aligned.
80
+ - If a change affects installed payload contents, bump
81
+ `assets/framework/VERSION` so packaged upgrades can detect it.
82
+ - Ownership behavior must stay identical across layout changes.
83
+ - The tree remains decision-focused; execution detail stays in source systems.
84
+ - A path migration is incomplete if task text, docs, tests, and runtime assets
85
+ disagree about where the framework lives.
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+
7
+ find_repo_root() {
8
+ local dir="$SKILL_DIR"
9
+ while [[ "$dir" != "/" ]]; do
10
+ if [[ -f "$dir/package.json" ]] && grep -q '"name": "first-tree"' "$dir/package.json"; then
11
+ printf '%s\n' "$dir"
12
+ return 0
13
+ fi
14
+ dir="$(dirname "$dir")"
15
+ done
16
+ return 1
17
+ }
18
+
19
+ require_file() {
20
+ local path="$1"
21
+ if [[ ! -f "$path" ]]; then
22
+ echo "Missing file: $path" >&2
23
+ exit 1
24
+ fi
25
+ }
26
+
27
+ REPO_ROOT="$(find_repo_root || true)"
28
+ SOURCE_DIR=""
29
+ if [[ -n "$REPO_ROOT" ]]; then
30
+ SOURCE_DIR="$REPO_ROOT/skills/first-tree"
31
+ fi
32
+
33
+ if [[ -z "$REPO_ROOT" || "$SKILL_DIR" != "$SOURCE_DIR" ]]; then
34
+ echo "Run this script from the source-of-truth skill at skills/first-tree inside a live first-tree checkout." >&2
35
+ exit 1
36
+ fi
37
+
38
+ require_file "$SOURCE_DIR/SKILL.md"
39
+ require_file "$SOURCE_DIR/agents/openai.yaml"
40
+ require_file "$SOURCE_DIR/references/about.md"
41
+ require_file "$SOURCE_DIR/references/onboarding.md"
42
+ require_file "$SOURCE_DIR/references/principles.md"
43
+ require_file "$SOURCE_DIR/references/ownership-and-naming.md"
44
+ require_file "$SOURCE_DIR/references/source-map.md"
45
+ require_file "$SOURCE_DIR/references/upgrade-contract.md"
46
+ require_file "$SOURCE_DIR/references/maintainer-architecture.md"
47
+ require_file "$SOURCE_DIR/references/maintainer-thin-cli.md"
48
+ require_file "$SOURCE_DIR/references/maintainer-build-and-distribution.md"
49
+ require_file "$SOURCE_DIR/references/maintainer-testing.md"
50
+ require_file "$SOURCE_DIR/engine/init.ts"
51
+ require_file "$SOURCE_DIR/engine/onboarding.ts"
52
+ require_file "$SOURCE_DIR/engine/repo.ts"
53
+ require_file "$SOURCE_DIR/engine/upgrade.ts"
54
+ require_file "$SOURCE_DIR/engine/verify.ts"
55
+ require_file "$SOURCE_DIR/engine/commands/help.ts"
56
+ require_file "$SOURCE_DIR/engine/commands/init.ts"
57
+ require_file "$SOURCE_DIR/engine/commands/upgrade.ts"
58
+ require_file "$SOURCE_DIR/engine/commands/verify.ts"
59
+ require_file "$SOURCE_DIR/engine/rules/index.ts"
60
+ require_file "$SOURCE_DIR/engine/runtime/asset-loader.ts"
61
+ require_file "$SOURCE_DIR/engine/runtime/installer.ts"
62
+ require_file "$SOURCE_DIR/engine/runtime/upgrader.ts"
63
+ require_file "$SOURCE_DIR/engine/runtime/adapters.ts"
64
+ require_file "$SOURCE_DIR/engine/validators/members.ts"
65
+ require_file "$SOURCE_DIR/engine/validators/nodes.ts"
66
+ require_file "$SOURCE_DIR/tests/init.test.ts"
67
+ require_file "$SOURCE_DIR/tests/verify.test.ts"
68
+ require_file "$SOURCE_DIR/tests/skill-artifacts.test.ts"
69
+ require_file "$SOURCE_DIR/assets/framework/manifest.json"
70
+ require_file "$SOURCE_DIR/assets/framework/VERSION"
71
+ require_file "$SOURCE_DIR/assets/framework/prompts/pr-review.md"
72
+ require_file "$SOURCE_DIR/assets/framework/templates/root-node.md.template"
73
+ require_file "$SOURCE_DIR/assets/framework/templates/agent.md.template"
74
+ require_file "$SOURCE_DIR/assets/framework/templates/members-domain.md.template"
75
+ require_file "$SOURCE_DIR/assets/framework/templates/member-node.md.template"
76
+ require_file "$SOURCE_DIR/assets/framework/workflows/validate.yml"
77
+ require_file "$SOURCE_DIR/assets/framework/workflows/pr-review.yml"
78
+ require_file "$SOURCE_DIR/assets/framework/workflows/codeowners.yml"
79
+ require_file "$SOURCE_DIR/assets/framework/examples/claude-code/README.md"
80
+ require_file "$SOURCE_DIR/assets/framework/examples/claude-code/settings.json"
81
+ require_file "$SOURCE_DIR/assets/framework/helpers/generate-codeowners.ts"
82
+ require_file "$SOURCE_DIR/assets/framework/helpers/run-review.ts"
83
+ require_file "$SOURCE_DIR/assets/framework/helpers/inject-tree-context.sh"
84
+
85
+ # Check for legacy artifacts that should not be committed.
86
+ # Use git ls-files to ignore untracked local files (e.g. .claude/settings.local.json).
87
+ for legacy_path in \
88
+ ".agents" \
89
+ ".claude" \
90
+ ".context-tree" \
91
+ "docs" \
92
+ "skills/first-tree-cli-framework" \
93
+ "tests" \
94
+ "skills/first-tree/references/repo-snapshot"
95
+ do
96
+ if git -C "$REPO_ROOT" ls-files --error-unmatch "$legacy_path" >/dev/null 2>&1; then
97
+ echo "Unexpected legacy artifact tracked in git: $legacy_path" >&2
98
+ exit 1
99
+ fi
100
+ done
101
+
102
+ if [[ -e "$SOURCE_DIR/evals" ]]; then
103
+ echo "Skill should not contain repo-only eval tooling." >&2
104
+ exit 1
105
+ fi
106
+
107
+ require_file "$REPO_ROOT/evals/context-tree-eval.test.ts"
108
+ require_file "$REPO_ROOT/evals/README.md"
109
+ require_file "$REPO_ROOT/evals/helpers/case-loader.ts"
110
+ require_file "$REPO_ROOT/evals/scripts/tree-manager.ts"
111
+ require_file "$REPO_ROOT/evals/tests/eval-helpers.test.ts"
112
+
113
+ if grep -q '"#docs/\*"' "$REPO_ROOT/package.json"; then
114
+ echo "package.json still exposes the legacy #docs import alias." >&2
115
+ exit 1
116
+ fi
117
+
118
+ if ! grep -q '"#skill/\*"' "$REPO_ROOT/package.json"; then
119
+ echo "package.json is missing the canonical #skill import alias." >&2
120
+ exit 1
121
+ fi
122
+
123
+ if ! grep -q '"skills/first-tree"' "$REPO_ROOT/package.json"; then
124
+ echo "package.json is missing the canonical skill in the published files list." >&2
125
+ exit 1
126
+ fi
127
+
128
+ if ! grep -q '#skill/engine/commands/init.js' "$REPO_ROOT/src/cli.ts"; then
129
+ echo "src/cli.ts is not dispatching to the skill-owned engine." >&2
130
+ exit 1
131
+ fi
132
+
133
+ echo "Canonical skill structure is clean."
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Portable quick validator for a skill directory.
4
+
5
+ This version is intentionally self-contained so it can run in CI and in copied
6
+ skill folders without depending on external Python packages.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ MAX_SKILL_NAME_LENGTH = 64
16
+
17
+
18
+ def extract_frontmatter(text: str) -> tuple[bool, str]:
19
+ if not text.startswith("---\n"):
20
+ return False, "No YAML frontmatter found"
21
+ match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
22
+ if not match:
23
+ return False, "Invalid frontmatter format"
24
+ return True, match.group(1)
25
+
26
+
27
+ def parse_simple_frontmatter(frontmatter: str) -> dict[str, str]:
28
+ data: dict[str, str] = {}
29
+ for line in frontmatter.splitlines():
30
+ stripped = line.strip()
31
+ if not stripped or stripped.startswith("#"):
32
+ continue
33
+ if ":" not in stripped:
34
+ continue
35
+ key, value = stripped.split(":", 1)
36
+ key = key.strip()
37
+ value = value.strip()
38
+ if value.startswith(("'", '"')) and value.endswith(("'", '"')) and len(value) >= 2:
39
+ value = value[1:-1]
40
+ data[key] = value
41
+ return data
42
+
43
+
44
+ def validate_skill(skill_path: str) -> tuple[bool, str]:
45
+ skill_dir = Path(skill_path)
46
+ skill_md = skill_dir / "SKILL.md"
47
+ if not skill_md.exists():
48
+ return False, "SKILL.md not found"
49
+
50
+ content = skill_md.read_text()
51
+ ok, frontmatter_or_error = extract_frontmatter(content)
52
+ if not ok:
53
+ return False, frontmatter_or_error
54
+
55
+ frontmatter = parse_simple_frontmatter(frontmatter_or_error)
56
+
57
+ unexpected = set(frontmatter.keys()) - {"name", "description"}
58
+ if unexpected:
59
+ return False, f"Unexpected key(s) in frontmatter: {', '.join(sorted(unexpected))}"
60
+
61
+ name = frontmatter.get("name", "").strip()
62
+ if not name:
63
+ return False, "Missing 'name' in frontmatter"
64
+ if not re.match(r"^[a-z0-9-]+$", name):
65
+ return False, f"Name '{name}' should be hyphen-case"
66
+ if name.startswith("-") or name.endswith("-") or "--" in name:
67
+ return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
68
+ if len(name) > MAX_SKILL_NAME_LENGTH:
69
+ return False, f"Name is too long ({len(name)} > {MAX_SKILL_NAME_LENGTH})"
70
+
71
+ description = frontmatter.get("description", "").strip()
72
+ if not description:
73
+ return False, "Missing 'description' in frontmatter"
74
+ if len(description) > 1024:
75
+ return False, f"Description is too long ({len(description)} > 1024)"
76
+
77
+ openai_yaml = skill_dir / "agents" / "openai.yaml"
78
+ if not openai_yaml.exists():
79
+ return False, "agents/openai.yaml not found"
80
+
81
+ return True, "Skill is valid!"
82
+
83
+
84
+ def main() -> int:
85
+ if len(sys.argv) != 2:
86
+ print("Usage: quick_validate.py <skill_directory>")
87
+ return 1
88
+
89
+ valid, message = validate_skill(sys.argv[1])
90
+ print(message)
91
+ return 0 if valid else 1
92
+
93
+
94
+ if __name__ == "__main__":
95
+ raise SystemExit(main())
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+ SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
6
+ INSTALL_GUIDE="${SKILL_DIR}/references/onboarding.md"
7
+
8
+ find_repo_root() {
9
+ local dir="$SKILL_DIR"
10
+ while [[ "$dir" != "/" ]]; do
11
+ if [[ -f "$dir/package.json" ]] && grep -q '"name": "first-tree"' "$dir/package.json"; then
12
+ printf '%s\n' "$dir"
13
+ return 0
14
+ fi
15
+ dir="$(dirname "$dir")"
16
+ done
17
+ return 1
18
+ }
19
+
20
+ REPO_ROOT="$(find_repo_root || true)"
21
+
22
+ if [[ -n "${REPO_ROOT}" ]]; then
23
+ cd "$REPO_ROOT"
24
+ pnpm build >/dev/null
25
+ exec node dist/cli.js "$@"
26
+ fi
27
+
28
+ if command -v context-tree >/dev/null 2>&1; then
29
+ exec context-tree "$@"
30
+ fi
31
+
32
+ echo "Could not find a live first-tree checkout or a 'context-tree' binary on PATH." >&2
33
+ echo "Install the npm package 'first-tree' if you want the portable runner to invoke the CLI outside the repo." >&2
34
+ echo "Read the onboarding guide at: ${INSTALL_GUIDE}" >&2
35
+ exit 1
@@ -0,0 +1,75 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ FRAMEWORK_VERSION,
6
+ INSTALLED_PROGRESS,
7
+ LEGACY_SKILL_PROGRESS,
8
+ LEGACY_SKILL_VERSION,
9
+ LEGACY_PROGRESS,
10
+ LEGACY_VERSION,
11
+ detectFrameworkLayout,
12
+ frameworkVersionCandidates,
13
+ progressFileCandidates,
14
+ resolveFirstExistingPath,
15
+ } from "#skill/engine/runtime/asset-loader.js";
16
+ import { useTmpDir } from "./helpers.js";
17
+
18
+ describe("asset-loader", () => {
19
+ it("prefers the installed skill layout when both layouts exist", () => {
20
+ const tmp = useTmpDir();
21
+ mkdirSync(join(tmp.path, "skills", "first-tree", "assets", "framework"), {
22
+ recursive: true,
23
+ });
24
+ mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
25
+ writeFileSync(join(tmp.path, FRAMEWORK_VERSION), "0.2.0\n");
26
+ writeFileSync(join(tmp.path, LEGACY_VERSION), "0.1.0\n");
27
+
28
+ expect(detectFrameworkLayout(tmp.path)).toBe("skill");
29
+ expect(resolveFirstExistingPath(tmp.path, [FRAMEWORK_VERSION, LEGACY_VERSION])).toBe(
30
+ FRAMEWORK_VERSION,
31
+ );
32
+ });
33
+
34
+ it("falls back to the legacy layout when the skill is not installed", () => {
35
+ const tmp = useTmpDir();
36
+ mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
37
+ writeFileSync(join(tmp.path, LEGACY_VERSION), "0.1.0\n");
38
+
39
+ expect(detectFrameworkLayout(tmp.path)).toBe("legacy");
40
+ });
41
+
42
+ it("detects the previous installed skill name before the .context-tree layout", () => {
43
+ const tmp = useTmpDir();
44
+ mkdirSync(
45
+ join(tmp.path, "skills", "first-tree-cli-framework", "assets", "framework"),
46
+ {
47
+ recursive: true,
48
+ },
49
+ );
50
+ mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
51
+ writeFileSync(join(tmp.path, LEGACY_SKILL_VERSION), "0.2.0\n");
52
+ writeFileSync(join(tmp.path, LEGACY_VERSION), "0.1.0\n");
53
+
54
+ expect(detectFrameworkLayout(tmp.path)).toBe("legacy-skill");
55
+ expect(
56
+ resolveFirstExistingPath(tmp.path, frameworkVersionCandidates()),
57
+ ).toBe(LEGACY_SKILL_VERSION);
58
+ });
59
+
60
+ it("prefers the installed progress file candidate", () => {
61
+ const tmp = useTmpDir();
62
+ mkdirSync(join(tmp.path, "skills", "first-tree"), { recursive: true });
63
+ mkdirSync(join(tmp.path, "skills", "first-tree-cli-framework"), {
64
+ recursive: true,
65
+ });
66
+ mkdirSync(join(tmp.path, ".context-tree"), { recursive: true });
67
+ writeFileSync(join(tmp.path, INSTALLED_PROGRESS), "new");
68
+ writeFileSync(join(tmp.path, LEGACY_SKILL_PROGRESS), "old-skill");
69
+ writeFileSync(join(tmp.path, LEGACY_PROGRESS), "old");
70
+
71
+ expect(resolveFirstExistingPath(tmp.path, progressFileCandidates())).toBe(
72
+ INSTALLED_PROGRESS,
73
+ );
74
+ });
75
+ });
@@ -0,0 +1,94 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ parseOwners,
6
+ resolveNodeOwners,
7
+ collectEntries,
8
+ formatOwners,
9
+ } from "../assets/framework/helpers/generate-codeowners.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
+ // --- parseOwners ---
20
+
21
+ describe("parseOwners", () => {
22
+ it("parses valid owners", () => {
23
+ const tmp = useTmpDir();
24
+ const p = write(tmp.path, "NODE.md", "---\nowners: [alice, bob]\n---\n");
25
+ expect(parseOwners(p)).toEqual(["alice", "bob"]);
26
+ });
27
+
28
+ it("handles empty owners", () => {
29
+ const tmp = useTmpDir();
30
+ const p = write(tmp.path, "NODE.md", "---\nowners: []\n---\n");
31
+ expect(parseOwners(p)).toEqual([]);
32
+ });
33
+
34
+ it("handles wildcard", () => {
35
+ const tmp = useTmpDir();
36
+ const p = write(tmp.path, "NODE.md", "---\nowners: [*]\n---\n");
37
+ expect(parseOwners(p)).toEqual(["*"]);
38
+ });
39
+
40
+ it("returns null for no frontmatter", () => {
41
+ const tmp = useTmpDir();
42
+ const p = write(tmp.path, "NODE.md", "# Just a heading\n");
43
+ expect(parseOwners(p)).toBeNull();
44
+ });
45
+ });
46
+
47
+ // --- resolveNodeOwners ---
48
+
49
+ describe("resolveNodeOwners", () => {
50
+ it("returns direct owners", () => {
51
+ const tmp = useTmpDir();
52
+ write(tmp.path, "NODE.md", "---\nowners: [root-owner]\n---\n");
53
+ write(tmp.path, "domain/NODE.md", "---\nowners: [domain-owner]\n---\n");
54
+ const cache = new Map<string, string[]>();
55
+ const result = resolveNodeOwners(join(tmp.path, "domain"), tmp.path, cache);
56
+ expect(result).toEqual(["domain-owner"]);
57
+ });
58
+
59
+ it("inherits from parent", () => {
60
+ const tmp = useTmpDir();
61
+ write(tmp.path, "NODE.md", "---\nowners: [root-owner]\n---\n");
62
+ write(tmp.path, "domain/NODE.md", "---\nowners: []\n---\n");
63
+ const cache = new Map<string, string[]>();
64
+ const result = resolveNodeOwners(join(tmp.path, "domain"), tmp.path, cache);
65
+ expect(result).toEqual(["root-owner"]);
66
+ });
67
+ });
68
+
69
+ // --- collectEntries ---
70
+
71
+ describe("collectEntries", () => {
72
+ it("excludes dot-prefixed dirs", () => {
73
+ const tmp = useTmpDir();
74
+ write(tmp.path, "NODE.md", "---\nowners: [root]\n---\n# Root\n");
75
+ write(tmp.path, "domain/NODE.md", "---\nowners: [alice]\n---\n# Domain\n");
76
+ write(tmp.path, ".hidden/NODE.md", "---\nowners: [secret]\n---\n# Hidden\n");
77
+ const entries = collectEntries(tmp.path);
78
+ const patterns = entries.map(([pat]) => pat);
79
+ expect(patterns.some((p) => p.includes("domain"))).toBe(true);
80
+ expect(patterns.some((p) => p.includes(".hidden"))).toBe(false);
81
+ });
82
+ });
83
+
84
+ // --- formatOwners ---
85
+
86
+ describe("formatOwners", () => {
87
+ it("deduplicates owners", () => {
88
+ expect(formatOwners(["alice", "bob", "alice"])).toBe("@alice @bob");
89
+ });
90
+
91
+ it("adds @ prefix", () => {
92
+ expect(formatOwners(["alice"])).toBe("@alice");
93
+ });
94
+ });
@@ -0,0 +1,149 @@
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
+ FRAMEWORK_VERSION,
7
+ LEGACY_SKILL_VERSION,
8
+ LEGACY_VERSION,
9
+ } from "#skill/engine/runtime/asset-loader.js";
10
+
11
+ interface TmpDir {
12
+ path: string;
13
+ }
14
+
15
+ export function useTmpDir(): TmpDir {
16
+ const dir = mkdtempSync(join(tmpdir(), "ct-test-"));
17
+ afterEach(() => {
18
+ rmSync(dir, { recursive: true, force: true });
19
+ });
20
+ return { path: dir };
21
+ }
22
+
23
+ export function makeFramework(root: string, version = "0.1.0"): void {
24
+ mkdirSync(join(root, "skills", "first-tree", "assets", "framework"), {
25
+ recursive: true,
26
+ });
27
+ writeFileSync(join(root, FRAMEWORK_VERSION), `${version}\n`);
28
+ }
29
+
30
+ export function makeLegacyFramework(root: string, version = "0.1.0"): void {
31
+ const ct = join(root, ".context-tree");
32
+ mkdirSync(ct, { recursive: true });
33
+ writeFileSync(join(root, LEGACY_VERSION), `${version}\n`);
34
+ }
35
+
36
+ export function makeLegacyNamedFramework(
37
+ root: string,
38
+ version = "0.1.0",
39
+ ): void {
40
+ mkdirSync(
41
+ join(root, "skills", "first-tree-cli-framework", "assets", "framework"),
42
+ {
43
+ recursive: true,
44
+ },
45
+ );
46
+ writeFileSync(join(root, LEGACY_SKILL_VERSION), `${version}\n`);
47
+ }
48
+
49
+ export function makeSourceSkill(root: string, version = "0.2.0"): void {
50
+ const skillRoot = join(root, "skills", "first-tree");
51
+ mkdirSync(join(skillRoot, "agents"), { recursive: true });
52
+ mkdirSync(join(skillRoot, "assets", "framework", "templates"), {
53
+ recursive: true,
54
+ });
55
+
56
+ writeFileSync(
57
+ join(skillRoot, "SKILL.md"),
58
+ "---\nname: first-tree\ndescription: test\n---\n",
59
+ );
60
+ writeFileSync(
61
+ join(skillRoot, "agents", "openai.yaml"),
62
+ "display_name: First Tree\nshort_description: test\n",
63
+ );
64
+ writeFileSync(
65
+ join(skillRoot, "assets", "framework", "manifest.json"),
66
+ "{}\n",
67
+ );
68
+ writeFileSync(
69
+ join(skillRoot, "assets", "framework", "VERSION"),
70
+ `${version}\n`,
71
+ );
72
+ writeFileSync(
73
+ join(
74
+ skillRoot,
75
+ "assets",
76
+ "framework",
77
+ "templates",
78
+ "root-node.md.template",
79
+ ),
80
+ "---\ntitle: Example Tree\nowners: [alice]\n---\n# Example Tree\n",
81
+ );
82
+ writeFileSync(
83
+ join(
84
+ skillRoot,
85
+ "assets",
86
+ "framework",
87
+ "templates",
88
+ "agent.md.template",
89
+ ),
90
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nframework text\n<!-- END CONTEXT-TREE FRAMEWORK -->\n",
91
+ );
92
+ writeFileSync(
93
+ join(
94
+ skillRoot,
95
+ "assets",
96
+ "framework",
97
+ "templates",
98
+ "members-domain.md.template",
99
+ ),
100
+ "---\ntitle: Members\nowners: [alice]\n---\n# Members\n",
101
+ );
102
+ }
103
+
104
+ export function makeNode(
105
+ root: string,
106
+ opts?: { placeholder?: boolean },
107
+ ): void {
108
+ const body = opts?.placeholder
109
+ ? "<!-- PLACEHOLDER -->\n"
110
+ : "# Real content\n";
111
+ writeFileSync(
112
+ join(root, "NODE.md"),
113
+ `---\ntitle: My Org\nowners: [alice]\n---\n${body}`,
114
+ );
115
+ }
116
+
117
+ export function makeAgentMd(
118
+ root: string,
119
+ opts?: { markers?: boolean; userContent?: boolean },
120
+ ): void {
121
+ const markers = opts?.markers ?? true;
122
+ const userContent = opts?.userContent ?? false;
123
+ const parts: string[] = [];
124
+ if (markers) {
125
+ parts.push(
126
+ "<!-- BEGIN CONTEXT-TREE FRAMEWORK -->\nframework stuff\n<!-- END CONTEXT-TREE FRAMEWORK -->",
127
+ );
128
+ } else {
129
+ parts.push("# Agent instructions\n");
130
+ }
131
+ if (userContent) {
132
+ parts.push("\n# Project-specific\nThis is real user content.\n");
133
+ }
134
+ writeFileSync(join(root, "AGENT.md"), parts.join("\n"));
135
+ }
136
+
137
+ export function makeMembers(root: string, count = 1): void {
138
+ const membersDir = join(root, "members");
139
+ mkdirSync(membersDir, { recursive: true });
140
+ writeFileSync(
141
+ join(membersDir, "NODE.md"),
142
+ "---\ntitle: Members\n---\n",
143
+ );
144
+ for (let i = 0; i < count; i++) {
145
+ const d = join(membersDir, `member-${i}`);
146
+ mkdirSync(d);
147
+ writeFileSync(join(d, "NODE.md"), `---\ntitle: Member ${i}\nowners: [member-${i}]\ntype: human\nrole: Engineer\ndomains:\n - engineering\n---\n`);
148
+ }
149
+ }