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.
- package/README.md +116 -40
- package/dist/cli.js +46 -17
- package/dist/help-Dtdj91HJ.js +25 -0
- package/dist/init--VepFe6N.js +403 -0
- package/dist/installer-cH7N4RNj.js +47 -0
- package/dist/onboarding-C9cYSE6F.js +2 -0
- package/dist/onboarding-CPP8fF4D.js +10 -0
- package/dist/repo-DY57bMqr.js +318 -0
- package/dist/upgrade-Cgx_K2HM.js +135 -0
- package/dist/{verify-CSRIkuoM.js → verify-mC9ZTd1f.js} +118 -29
- package/package.json +33 -10
- package/skills/first-tree/SKILL.md +113 -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 +193 -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/agents.md.template +49 -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 +41 -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 +414 -0
- package/skills/first-tree/engine/onboarding.ts +10 -0
- package/skills/first-tree/engine/repo.ts +360 -0
- package/skills/first-tree/engine/rules/agent-instructions.ts +59 -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 +141 -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 +233 -0
- package/skills/first-tree/engine/validators/members.ts +215 -0
- package/skills/first-tree/engine/validators/nodes.ts +559 -0
- package/skills/first-tree/engine/verify.ts +155 -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 +59 -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 +185 -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 +94 -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 +169 -0
- package/skills/first-tree/tests/init.test.ts +250 -0
- package/skills/first-tree/tests/repo.test.ts +440 -0
- package/skills/first-tree/tests/rules.test.ts +413 -0
- package/skills/first-tree/tests/run-review.test.ts +155 -0
- package/skills/first-tree/tests/skill-artifacts.test.ts +311 -0
- package/skills/first-tree/tests/thin-cli.test.ts +104 -0
- package/skills/first-tree/tests/upgrade.test.ts +103 -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 +241 -0
- package/dist/init-CE_944sb.js +0 -283
- package/dist/repo-BByc3VvM.js +0 -111
- package/dist/upgrade-Chr7z0CY.js +0 -82
package/package.json
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "first-tree",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "CLI tools for Context Tree — the living source of truth for your organization.",
|
|
5
|
+
"homepage": "https://github.com/agent-team-foundation/first-tree#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/agent-team-foundation/first-tree/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/agent-team-foundation/first-tree.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"context-tree",
|
|
15
|
+
"cli",
|
|
16
|
+
"agents",
|
|
17
|
+
"knowledge-base",
|
|
18
|
+
"markdown"
|
|
19
|
+
],
|
|
5
20
|
"type": "module",
|
|
6
21
|
"bin": {
|
|
7
22
|
"context-tree": "./dist/cli.js"
|
|
8
23
|
},
|
|
24
|
+
"imports": {
|
|
25
|
+
"#skill/*": "./skills/first-tree/*",
|
|
26
|
+
"#evals/*": "./evals/*",
|
|
27
|
+
"#src/*": "./src/*"
|
|
28
|
+
},
|
|
9
29
|
"files": [
|
|
10
|
-
"dist"
|
|
30
|
+
"dist",
|
|
31
|
+
"skills/first-tree"
|
|
11
32
|
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsdown src/cli.ts --format esm --out-dir dist",
|
|
14
|
-
"prepack": "pnpm build",
|
|
15
|
-
"test": "vitest run",
|
|
16
|
-
"typecheck": "tsc --noEmit"
|
|
17
|
-
},
|
|
18
33
|
"license": "Apache-2.0",
|
|
19
34
|
"devDependencies": {
|
|
20
35
|
"@types/node": "^25.5.0",
|
|
21
36
|
"tsdown": "^0.12.0",
|
|
22
37
|
"typescript": "^5.8.0",
|
|
23
|
-
"vitest": "^3.2.0"
|
|
38
|
+
"vitest": "^3.2.0",
|
|
39
|
+
"yaml": "^2.8.3"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsdown",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"eval": "vitest run --config vitest.eval.config.ts",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"validate:skill": "python3 ./skills/first-tree/scripts/quick_validate.py ./skills/first-tree && bash ./skills/first-tree/scripts/check-skill-sync.sh"
|
|
24
47
|
}
|
|
25
|
-
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: first-tree
|
|
3
|
+
description: Maintain the canonical `first-tree` skill and the thin `context-tree` CLI distributed by the `first-tree` npm package. Use when modifying `context-tree` commands (`init`, `verify`, `upgrade`, `help onboarding`), the installed skill payload under `assets/framework/`, maintainer references, or the build, packaging, test, and CI wiring that supports the framework.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# First Tree
|
|
7
|
+
|
|
8
|
+
Use this skill when the task depends on the exact behavior of the
|
|
9
|
+
`context-tree` CLI or the installed `skills/first-tree/` payload that
|
|
10
|
+
`context-tree init` ships to user repos.
|
|
11
|
+
|
|
12
|
+
## Source Of Truth
|
|
13
|
+
|
|
14
|
+
- `skills/first-tree/` is the only canonical copy.
|
|
15
|
+
- `references/` holds the explanatory docs the skill should load on demand.
|
|
16
|
+
- `assets/framework/` holds the runtime payload that gets installed into user
|
|
17
|
+
repos.
|
|
18
|
+
- `engine/` holds the canonical framework and CLI behavior.
|
|
19
|
+
- `scripts/` holds maintenance helpers for validating and running the skill.
|
|
20
|
+
- In maintainer docs, use `context-tree` for the CLI and `skills/first-tree/`
|
|
21
|
+
for the installed skill path so it is not confused with the `first-tree`
|
|
22
|
+
npm package.
|
|
23
|
+
|
|
24
|
+
## When To Read What
|
|
25
|
+
|
|
26
|
+
1. Start with `references/source-map.md` to locate the right files.
|
|
27
|
+
2. Read the user-facing reference that matches the task:
|
|
28
|
+
- `references/onboarding.md`
|
|
29
|
+
- `references/about.md`
|
|
30
|
+
- `references/principles.md`
|
|
31
|
+
- `references/ownership-and-naming.md`
|
|
32
|
+
- `references/upgrade-contract.md`
|
|
33
|
+
3. Read the maintainer reference that matches the shell or validation surface:
|
|
34
|
+
- `references/maintainer-architecture.md`
|
|
35
|
+
- `references/maintainer-thin-cli.md`
|
|
36
|
+
- `references/maintainer-build-and-distribution.md`
|
|
37
|
+
- `references/maintainer-testing.md`
|
|
38
|
+
4. Open `engine/` when changing `init`, `verify`, `upgrade`, command routing,
|
|
39
|
+
repo inspection, rules, runtime helpers, or validators.
|
|
40
|
+
5. Open `assets/framework/` only when the task changes shipped templates,
|
|
41
|
+
workflows, prompts, examples, or helper scripts.
|
|
42
|
+
6. Open `tests/` when changing validation coverage or maintainer workflows.
|
|
43
|
+
7. Use `./scripts/run-local-cli.sh <command>` when you need to exercise the
|
|
44
|
+
live CLI from this repo.
|
|
45
|
+
|
|
46
|
+
## Working Modes
|
|
47
|
+
|
|
48
|
+
### Maintaining `first-tree`
|
|
49
|
+
|
|
50
|
+
- Treat this repo as the distribution source for one canonical skill plus a
|
|
51
|
+
thin CLI shell, not as a tree repo.
|
|
52
|
+
- Keep command behavior, validator behavior, shipped assets, maintainer
|
|
53
|
+
references, and package shell aligned.
|
|
54
|
+
- If root README/AGENTS/CI text explains something non-obvious, migrate that
|
|
55
|
+
information into `references/` and trim the root file back down.
|
|
56
|
+
- If you change runtime assets or skill references, run `pnpm validate:skill`.
|
|
57
|
+
|
|
58
|
+
### Working In A User Tree Repo
|
|
59
|
+
|
|
60
|
+
- `context-tree init` defaults to creating or reusing a sibling dedicated tree
|
|
61
|
+
repo when invoked from a source/workspace repo. Use `--here` to initialize
|
|
62
|
+
the current repo in place when you are already inside the tree repo.
|
|
63
|
+
- `context-tree init` installs this skill into the target tree repo and
|
|
64
|
+
`NODE.md`, `AGENTS.md`, and `members/NODE.md`.
|
|
65
|
+
- `context-tree upgrade` refreshes the installed skill from the copy bundled
|
|
66
|
+
with the currently running `first-tree` package. To pick up a newer
|
|
67
|
+
framework, run a newer package version first. It also migrates older repos
|
|
68
|
+
that still use `skills/first-tree-cli-framework/`.
|
|
69
|
+
- The user's tree content lives outside the skill; the skill only carries the
|
|
70
|
+
reusable framework payload plus maintenance guidance.
|
|
71
|
+
- The tree still stores decisions, constraints, and ownership; execution detail
|
|
72
|
+
stays in source systems.
|
|
73
|
+
|
|
74
|
+
## Non-Negotiables
|
|
75
|
+
|
|
76
|
+
- Preserve the CLI contract that it scaffolds, prints task lists, and validates
|
|
77
|
+
state; it does not fully automate tree maintenance.
|
|
78
|
+
- Keep shipped assets generic. They must not contain org-specific content.
|
|
79
|
+
- Keep decision knowledge in the tree and execution detail in source systems.
|
|
80
|
+
- Keep the skill as the only canonical knowledge source. The root CLI/package
|
|
81
|
+
shell must not become a second source of framework semantics.
|
|
82
|
+
- Keep normal `init` / `upgrade` flows self-contained. They must work from the
|
|
83
|
+
skill bundled in the current package without cloning the source repo or
|
|
84
|
+
relying on network access.
|
|
85
|
+
- Make upgrade behavior explicit. If you change installed paths, update
|
|
86
|
+
`references/upgrade-contract.md`, task text, and tests together.
|
|
87
|
+
|
|
88
|
+
## Validation
|
|
89
|
+
|
|
90
|
+
- Repo checks: `pnpm typecheck`, `pnpm test`, `pnpm build`
|
|
91
|
+
- Packaging check: `pnpm pack` when changing package contents or install/upgrade
|
|
92
|
+
behavior
|
|
93
|
+
- Skill checks:
|
|
94
|
+
- `pnpm validate:skill`
|
|
95
|
+
- `python3 ./skills/first-tree/scripts/quick_validate.py ./skills/first-tree`
|
|
96
|
+
- `bash ./skills/first-tree/scripts/check-skill-sync.sh`
|
|
97
|
+
|
|
98
|
+
## Key Files
|
|
99
|
+
|
|
100
|
+
- `assets/framework/manifest.json`: runtime asset contract
|
|
101
|
+
- `assets/framework/templates/`: generated scaffolds
|
|
102
|
+
- `assets/framework/workflows/`: CI templates
|
|
103
|
+
- `assets/framework/helpers/`: shipped helper scripts and review tooling
|
|
104
|
+
- `engine/`: canonical framework and CLI behavior
|
|
105
|
+
- `tests/`: canonical unit and structure validation
|
|
106
|
+
- `references/source-map.md`: canonical reading index
|
|
107
|
+
- `references/maintainer-architecture.md`: source-repo architecture and
|
|
108
|
+
invariants
|
|
109
|
+
- `references/maintainer-thin-cli.md`: root shell contract
|
|
110
|
+
- `references/maintainer-build-and-distribution.md`: packaging and release
|
|
111
|
+
guidance
|
|
112
|
+
- `references/maintainer-testing.md`: validation workflow
|
|
113
|
+
- `references/upgrade-contract.md`: installed layout and upgrade semantics
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "First Tree"
|
|
3
|
+
short_description: "Maintain the First Tree skill and thin Context Tree CLI"
|
|
4
|
+
default_prompt: "Use $first-tree to maintain the canonical first-tree skill, its thin context-tree CLI, or its build, packaging, test, eval, and CI wiring."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Claude Code Integration
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Copy `settings.json` to your tree repo's `.claude/` directory:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
mkdir -p .claude
|
|
9
|
+
cp skills/first-tree/assets/framework/examples/claude-code/settings.json .claude/settings.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What It Does
|
|
13
|
+
|
|
14
|
+
The `SessionStart` hook runs `./skills/first-tree/assets/framework/helpers/inject-tree-context.sh` when a Claude Code session begins. This injects the root `NODE.md` content as additional context, giving the agent an overview of the tree structure before any task.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
statSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { dirname, join, relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
12
|
+
const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
|
|
13
|
+
const SKIP = new Set(["node_modules", "__pycache__"]);
|
|
14
|
+
|
|
15
|
+
export function parseOwners(path: string): string[] | null {
|
|
16
|
+
let text: string;
|
|
17
|
+
try {
|
|
18
|
+
text = readFileSync(path, "utf-8");
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const fm = text.match(FRONTMATTER_RE);
|
|
23
|
+
if (!fm) return null;
|
|
24
|
+
const m = fm[1].match(OWNERS_RE);
|
|
25
|
+
if (!m) return null;
|
|
26
|
+
const raw = m[1].trim();
|
|
27
|
+
if (!raw) return []; // owners: [] — will inherit
|
|
28
|
+
return raw
|
|
29
|
+
.split(",")
|
|
30
|
+
.map((o) => o.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveNodeOwners(
|
|
35
|
+
folder: string,
|
|
36
|
+
treeRoot: string,
|
|
37
|
+
cache: Map<string, string[]>,
|
|
38
|
+
): string[] {
|
|
39
|
+
if (cache.has(folder)) return cache.get(folder)!;
|
|
40
|
+
|
|
41
|
+
const nodeMd = join(folder, "NODE.md");
|
|
42
|
+
const owners = parseOwners(nodeMd);
|
|
43
|
+
|
|
44
|
+
let resolved: string[];
|
|
45
|
+
if (owners === null || owners.length === 0) {
|
|
46
|
+
const parent = dirname(folder);
|
|
47
|
+
if (parent.length >= treeRoot.length && parent !== folder) {
|
|
48
|
+
resolved = resolveNodeOwners(parent, treeRoot, cache);
|
|
49
|
+
} else {
|
|
50
|
+
resolved = [];
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
resolved = owners;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
cache.set(folder, resolved);
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isWildcard(owners: string[] | null): boolean {
|
|
61
|
+
return owners !== null && owners.includes("*");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function codeownersPath(path: string, treeRoot: string): string {
|
|
65
|
+
const r = relative(treeRoot, path).replace(/\\/g, "/");
|
|
66
|
+
try {
|
|
67
|
+
if (statSync(path).isDirectory()) return `/${r}/`;
|
|
68
|
+
} catch {
|
|
69
|
+
// not a dir
|
|
70
|
+
}
|
|
71
|
+
return `/${r}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function formatOwners(owners: string[]): string {
|
|
75
|
+
const seen = new Set<string>();
|
|
76
|
+
const result: string[] = [];
|
|
77
|
+
for (const o of owners) {
|
|
78
|
+
if (!seen.has(o)) {
|
|
79
|
+
seen.add(o);
|
|
80
|
+
result.push(`@${o}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result.join(" ");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collectEntries(
|
|
87
|
+
root: string,
|
|
88
|
+
): [string, string[]][] {
|
|
89
|
+
const nodeCache = new Map<string, string[]>();
|
|
90
|
+
const entries: [string, string[]][] = [];
|
|
91
|
+
|
|
92
|
+
function walk(dir: string): void {
|
|
93
|
+
let names: string[];
|
|
94
|
+
try {
|
|
95
|
+
names = readdirSync(dir).sort();
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const name of names) {
|
|
100
|
+
const full = join(dir, name);
|
|
101
|
+
const parts = relative(root, full).split("/");
|
|
102
|
+
if (parts.some((p) => SKIP.has(p) || p.startsWith("."))) continue;
|
|
103
|
+
try {
|
|
104
|
+
if (!statSync(full).isDirectory()) continue;
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!existsSync(join(full, "NODE.md"))) {
|
|
109
|
+
walk(full);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const folderOwners = resolveNodeOwners(full, root, nodeCache);
|
|
114
|
+
|
|
115
|
+
if (folderOwners.length > 0 && !isWildcard(folderOwners)) {
|
|
116
|
+
entries.push([codeownersPath(full, root), folderOwners]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Leaf files
|
|
120
|
+
for (const child of readdirSync(full).sort()) {
|
|
121
|
+
const childPath = join(full, child);
|
|
122
|
+
try {
|
|
123
|
+
if (
|
|
124
|
+
!statSync(childPath).isFile() ||
|
|
125
|
+
!child.endsWith(".md") ||
|
|
126
|
+
child === "NODE.md"
|
|
127
|
+
)
|
|
128
|
+
continue;
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const leafOwners = parseOwners(childPath);
|
|
133
|
+
if (isWildcard(leafOwners)) continue;
|
|
134
|
+
if (leafOwners && leafOwners.length > 0) {
|
|
135
|
+
const nonWildcardFolder = folderOwners.filter((o) => o !== "*");
|
|
136
|
+
const combined = [
|
|
137
|
+
...nonWildcardFolder,
|
|
138
|
+
...leafOwners.filter((o) => !nonWildcardFolder.includes(o)),
|
|
139
|
+
];
|
|
140
|
+
if (combined.length > 0) {
|
|
141
|
+
entries.push([codeownersPath(childPath, root), combined]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
walk(full);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
walk(root);
|
|
151
|
+
|
|
152
|
+
// Root-level leaf files
|
|
153
|
+
const rootOwners = resolveNodeOwners(root, root, nodeCache);
|
|
154
|
+
for (const child of readdirSync(root).sort()) {
|
|
155
|
+
const childPath = join(root, child);
|
|
156
|
+
try {
|
|
157
|
+
if (
|
|
158
|
+
!statSync(childPath).isFile() ||
|
|
159
|
+
!child.endsWith(".md") ||
|
|
160
|
+
child === "NODE.md"
|
|
161
|
+
)
|
|
162
|
+
continue;
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const leafOwners = parseOwners(childPath);
|
|
167
|
+
if (isWildcard(leafOwners)) continue;
|
|
168
|
+
if (leafOwners && leafOwners.length > 0) {
|
|
169
|
+
const combined = [
|
|
170
|
+
...rootOwners,
|
|
171
|
+
...leafOwners.filter((o) => !rootOwners.includes(o)),
|
|
172
|
+
];
|
|
173
|
+
entries.push([codeownersPath(childPath, root), combined]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Root entry (catch-all)
|
|
178
|
+
if (rootOwners.length > 0) {
|
|
179
|
+
entries.unshift(["/*", rootOwners]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return entries;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function generate(
|
|
186
|
+
treeRoot: string,
|
|
187
|
+
opts?: { check?: boolean },
|
|
188
|
+
): number {
|
|
189
|
+
const check = opts?.check ?? false;
|
|
190
|
+
const entries = collectEntries(treeRoot);
|
|
191
|
+
const codeownersFile = join(treeRoot, ".github", "CODEOWNERS");
|
|
192
|
+
|
|
193
|
+
const lines = ["# Auto-generated from Context Tree. Do not edit manually.", ""];
|
|
194
|
+
for (const [pattern, owners] of entries) {
|
|
195
|
+
if (owners.length > 0) {
|
|
196
|
+
lines.push(`${pattern.padEnd(50)} ${formatOwners(owners)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
lines.push(""); // trailing newline
|
|
200
|
+
const content = lines.join("\n");
|
|
201
|
+
|
|
202
|
+
if (check) {
|
|
203
|
+
if (existsSync(codeownersFile) && readFileSync(codeownersFile, "utf-8") === content) {
|
|
204
|
+
console.log("CODEOWNERS is up-to-date.");
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
console.log(
|
|
208
|
+
"CODEOWNERS is out-of-date. Run: npx tsx skills/first-tree/assets/framework/helpers/generate-codeowners.ts",
|
|
209
|
+
);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mkdirSync(dirname(codeownersFile), { recursive: true });
|
|
214
|
+
writeFileSync(codeownersFile, content);
|
|
215
|
+
console.log(`Wrote ${relative(treeRoot, codeownersFile)}`);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const isDirectRun =
|
|
220
|
+
process.argv[1]?.endsWith("generate-codeowners.ts") ||
|
|
221
|
+
process.argv[1]?.endsWith("generate-codeowners.js");
|
|
222
|
+
if (isDirectRun) {
|
|
223
|
+
process.exit(generate(process.cwd()));
|
|
224
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Injects NODE.md content as additionalContext at session start.
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
TREE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
6
|
+
NODE_MD="$TREE_ROOT/NODE.md"
|
|
7
|
+
|
|
8
|
+
if [ -f "$NODE_MD" ]; then
|
|
9
|
+
# Escape for JSON: backslashes, double quotes, newlines, tabs, carriage returns
|
|
10
|
+
CONTENT=$(sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' -e 's/\r/\\r/g' "$NODE_MD" | awk '{printf "%s\\n", $0}' | sed 's/\\n$//')
|
|
11
|
+
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"${CONTENT}\"}}"
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
exit 0
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Run Claude Code review and extract structured JSON output.
|
|
4
|
+
*
|
|
5
|
+
* Builds the review prompt, invokes Claude Code with stream-json output,
|
|
6
|
+
* extracts text from the stream, parses the review JSON, and retries up
|
|
7
|
+
* to 3 times on failure. Writes the validated review JSON to /tmp/review.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
const CLAUDE_BIN = join(homedir(), ".local", "bin", "claude");
|
|
16
|
+
const MAX_ATTEMPTS = 3;
|
|
17
|
+
// Per-invocation budget cap. Worst case is $1.50 total (3 × $0.50),
|
|
18
|
+
// though retries are cheap in practice due to cached context via --continue.
|
|
19
|
+
const MAX_BUDGET_USD = 0.5;
|
|
20
|
+
const AGENT_INSTRUCTIONS_PATHS = ["AGENTS.md", "AGENT.md"] as const;
|
|
21
|
+
|
|
22
|
+
function resolveAgentInstructionsPath(): string {
|
|
23
|
+
for (const candidate of AGENT_INSTRUCTIONS_PATHS) {
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
return candidate;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Missing AGENTS.md in repo root (legacy AGENT.md is also accepted during migration).",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildPrompt(diffPath: string): string {
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
const agentInstructionsPath = resolveAgentInstructionsPath();
|
|
37
|
+
const files: [string, string][] = [
|
|
38
|
+
[agentInstructionsPath, agentInstructionsPath],
|
|
39
|
+
["Root NODE.md", "NODE.md"],
|
|
40
|
+
[
|
|
41
|
+
"Review Instructions",
|
|
42
|
+
"skills/first-tree/assets/framework/prompts/pr-review.md",
|
|
43
|
+
],
|
|
44
|
+
];
|
|
45
|
+
for (const [heading, path] of files) {
|
|
46
|
+
const content = readFileSync(path, "utf-8");
|
|
47
|
+
parts.push(`## ${heading}\n\n${content}`);
|
|
48
|
+
}
|
|
49
|
+
const diff = readFileSync(diffPath, "utf-8");
|
|
50
|
+
parts.push(`## PR Diff\n\n\`\`\`\n${diff}\`\`\``);
|
|
51
|
+
return parts.join("\n\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function extractStreamText(jsonl: string): string {
|
|
55
|
+
const textParts: string[] = [];
|
|
56
|
+
let resultText = "";
|
|
57
|
+
for (const line of jsonl.split("\n")) {
|
|
58
|
+
const trimmed = line.trim();
|
|
59
|
+
if (!trimmed) continue;
|
|
60
|
+
let msg: Record<string, unknown>;
|
|
61
|
+
try {
|
|
62
|
+
msg = JSON.parse(trimmed);
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (msg.type === "assistant") {
|
|
67
|
+
const message = msg.message as Record<string, unknown> | undefined;
|
|
68
|
+
const content = message?.content as Array<Record<string, unknown>> | undefined;
|
|
69
|
+
if (content) {
|
|
70
|
+
for (const block of content) {
|
|
71
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
72
|
+
textParts.push(block.text);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (msg.type === "result") {
|
|
78
|
+
const r = msg.result;
|
|
79
|
+
if (typeof r === "string" && r) {
|
|
80
|
+
resultText = r;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Prefer assistant text blocks; fall back to result field
|
|
85
|
+
return textParts.length > 0 ? textParts.join("") : resultText;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runClaude(opts: { prompt?: string; continueSession?: boolean }): string {
|
|
89
|
+
const cmd = [
|
|
90
|
+
"-p",
|
|
91
|
+
"--dangerously-skip-permissions",
|
|
92
|
+
"--output-format", "stream-json",
|
|
93
|
+
"--verbose",
|
|
94
|
+
"--max-budget-usd", String(MAX_BUDGET_USD),
|
|
95
|
+
];
|
|
96
|
+
if (opts.continueSession) {
|
|
97
|
+
cmd.push("--continue");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const stdout = execFileSync(CLAUDE_BIN, cmd, {
|
|
102
|
+
input: opts.prompt,
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
105
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
106
|
+
});
|
|
107
|
+
return extractStreamText(stdout);
|
|
108
|
+
} catch (err: unknown) {
|
|
109
|
+
const code = (err as { status?: number }).status ?? 1;
|
|
110
|
+
console.error(`::error::Claude exited with code ${code}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface Review {
|
|
116
|
+
verdict: string;
|
|
117
|
+
summary?: string;
|
|
118
|
+
inline_comments?: Array<{ file: string; line: number; comment: string }>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function extractReviewJson(text: string): Review | null {
|
|
122
|
+
if (!text.trim()) return null;
|
|
123
|
+
// Strip markdown fences
|
|
124
|
+
let cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "");
|
|
125
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
126
|
+
if (!match) return null;
|
|
127
|
+
let obj: Record<string, unknown>;
|
|
128
|
+
try {
|
|
129
|
+
obj = JSON.parse(match[0]);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (!obj.verdict) return null;
|
|
134
|
+
return obj as unknown as Review;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function main(): void {
|
|
138
|
+
const prompt = buildPrompt("/tmp/pr-diff.txt");
|
|
139
|
+
console.log(`=== Prompt size: ${Buffer.byteLength(prompt)} bytes ===`);
|
|
140
|
+
|
|
141
|
+
let text = runClaude({ prompt });
|
|
142
|
+
|
|
143
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
144
|
+
if (text.trim()) {
|
|
145
|
+
console.log(`=== Attempt ${attempt}: Raw output ===`);
|
|
146
|
+
console.log(text);
|
|
147
|
+
console.log("=== End raw output ===");
|
|
148
|
+
} else {
|
|
149
|
+
console.log(`=== Attempt ${attempt}: Empty output ===`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const review = extractReviewJson(text);
|
|
153
|
+
if (review) {
|
|
154
|
+
console.log(
|
|
155
|
+
`Valid JSON with verdict='${review.verdict}' extracted on attempt ${attempt}`,
|
|
156
|
+
);
|
|
157
|
+
writeFileSync("/tmp/review.json", JSON.stringify(review));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (attempt === MAX_ATTEMPTS) {
|
|
162
|
+
console.error(
|
|
163
|
+
`::error::Failed to extract valid review JSON after ${MAX_ATTEMPTS} attempts`,
|
|
164
|
+
);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let retryMsg: string;
|
|
169
|
+
if (text.trim()) {
|
|
170
|
+
retryMsg =
|
|
171
|
+
"Your previous output could not be parsed as valid review JSON. " +
|
|
172
|
+
"Please output ONLY a valid JSON object matching the required schema " +
|
|
173
|
+
"(with verdict, optional summary, optional inline_comments). " +
|
|
174
|
+
"No other text, no markdown fences.";
|
|
175
|
+
} else {
|
|
176
|
+
retryMsg =
|
|
177
|
+
"You did not produce any visible text output. " +
|
|
178
|
+
"Please output ONLY the review as a valid JSON object with " +
|
|
179
|
+
"verdict (required), summary (optional), and inline_comments (optional). " +
|
|
180
|
+
"No other text, no markdown fences.";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`::warning::Attempt ${attempt} failed, asking Claude to retry...`);
|
|
184
|
+
text = runClaude({ prompt: retryMsg, continueSession: true });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isDirectRun =
|
|
189
|
+
process.argv[1]?.endsWith("run-review.ts") ||
|
|
190
|
+
process.argv[1]?.endsWith("run-review.js");
|
|
191
|
+
if (isDirectRun) {
|
|
192
|
+
main();
|
|
193
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"skillName": "first-tree",
|
|
4
|
+
"runtimeAssetRoot": "assets/framework",
|
|
5
|
+
"versionFile": "VERSION",
|
|
6
|
+
"templatesDir": "templates",
|
|
7
|
+
"workflowsDir": "workflows",
|
|
8
|
+
"promptsDir": "prompts",
|
|
9
|
+
"examplesDir": "examples",
|
|
10
|
+
"helpersDir": "helpers"
|
|
11
|
+
}
|