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,146 @@
|
|
|
1
|
+
name: PR Review
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [opened, reopened, synchronize]
|
|
6
|
+
issue_comment:
|
|
7
|
+
types: [created]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
review:
|
|
11
|
+
if: >
|
|
12
|
+
github.event_name == 'pull_request' ||
|
|
13
|
+
(github.event_name == 'issue_comment' &&
|
|
14
|
+
github.event.issue.pull_request &&
|
|
15
|
+
contains(github.event.comment.body, '@claude'))
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
pull-requests: write
|
|
20
|
+
issues: write
|
|
21
|
+
env:
|
|
22
|
+
ANTHROPIC_BASE_URL: "https://openrouter.ai/api"
|
|
23
|
+
ANTHROPIC_AUTH_TOKEN: ${{ secrets.OPENROUTER_API_KEY }}
|
|
24
|
+
ANTHROPIC_API_KEY: ""
|
|
25
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: "anthropic/claude-sonnet-4.6"
|
|
26
|
+
GH_TOKEN: ${{ github.token }}
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
with:
|
|
30
|
+
fetch-depth: 0
|
|
31
|
+
|
|
32
|
+
- uses: actions/setup-node@v4
|
|
33
|
+
with:
|
|
34
|
+
node-version: "22"
|
|
35
|
+
|
|
36
|
+
- name: Install Claude Code
|
|
37
|
+
run: curl -fsSL https://claude.ai/install.sh | bash
|
|
38
|
+
|
|
39
|
+
- name: Get PR info
|
|
40
|
+
id: pr
|
|
41
|
+
run: |
|
|
42
|
+
PR_NUMBER=${{ github.event.pull_request.number || github.event.issue.number }}
|
|
43
|
+
echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
|
|
44
|
+
gh pr diff "$PR_NUMBER" > /tmp/pr-diff.txt
|
|
45
|
+
gh pr view "$PR_NUMBER" --json headRefOid -q .headRefOid > /tmp/pr-head-sha.txt
|
|
46
|
+
|
|
47
|
+
- name: Run Claude review
|
|
48
|
+
run: npx tsx skills/first-tree/assets/framework/helpers/run-review.ts
|
|
49
|
+
|
|
50
|
+
- name: Parse and post review
|
|
51
|
+
run: |
|
|
52
|
+
PR_NUMBER=${{ steps.pr.outputs.number }}
|
|
53
|
+
HEAD_SHA=$(cat /tmp/pr-head-sha.txt)
|
|
54
|
+
|
|
55
|
+
# Extract fields
|
|
56
|
+
VERDICT=$(jq -r '.verdict // "COMMENT"' /tmp/review.json)
|
|
57
|
+
SUMMARY=$(jq -r '.summary // "Review completed."' /tmp/review.json)
|
|
58
|
+
|
|
59
|
+
case "$VERDICT" in
|
|
60
|
+
APPROVE) EVENT="APPROVE" ;;
|
|
61
|
+
REQUEST_CHANGES) EVENT="REQUEST_CHANGES" ;;
|
|
62
|
+
*) EVENT="COMMENT" ;;
|
|
63
|
+
esac
|
|
64
|
+
|
|
65
|
+
BODY="${SUMMARY}
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
*Automated review by Claude Code via OpenRouter*"
|
|
69
|
+
|
|
70
|
+
# Check for inline comments
|
|
71
|
+
HAS_COMMENTS=$(jq 'has("inline_comments") and (.inline_comments | length > 0)' /tmp/review.json)
|
|
72
|
+
|
|
73
|
+
if [ "$HAS_COMMENTS" != "true" ]; then
|
|
74
|
+
gh api \
|
|
75
|
+
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews" \
|
|
76
|
+
-f body="$BODY" \
|
|
77
|
+
-f event="$EVENT" \
|
|
78
|
+
-f commit_id="$HEAD_SHA"
|
|
79
|
+
else
|
|
80
|
+
# Transform inline_comments to GitHub API format (file->path, comment->body)
|
|
81
|
+
jq '[.inline_comments[] | {path: .file, line: .line, body: .comment}]' \
|
|
82
|
+
/tmp/review.json > /tmp/comments.json
|
|
83
|
+
|
|
84
|
+
jq -n \
|
|
85
|
+
--arg body "$BODY" \
|
|
86
|
+
--arg event "$EVENT" \
|
|
87
|
+
--arg sha "$HEAD_SHA" \
|
|
88
|
+
--slurpfile comments /tmp/comments.json \
|
|
89
|
+
'{
|
|
90
|
+
body: $body,
|
|
91
|
+
event: $event,
|
|
92
|
+
commit_id: $sha,
|
|
93
|
+
comments: $comments[0]
|
|
94
|
+
}' > /tmp/review-payload.json
|
|
95
|
+
|
|
96
|
+
# Try inline review; fall back to summary-only if lines can't be resolved
|
|
97
|
+
if ! gh api \
|
|
98
|
+
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews" \
|
|
99
|
+
--input /tmp/review-payload.json 2>/tmp/review-api-error.txt; then
|
|
100
|
+
echo "Inline review failed, falling back to summary-only review"
|
|
101
|
+
cat /tmp/review-api-error.txt
|
|
102
|
+
INLINE_TEXT=$(jq -r '.[] | "**\(.path):\(.line)** — \(.body)"' /tmp/comments.json)
|
|
103
|
+
FALLBACK_BODY="${SUMMARY}
|
|
104
|
+
|
|
105
|
+
### Inline Comments
|
|
106
|
+
|
|
107
|
+
${INLINE_TEXT}
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
*Automated review by Claude Code via OpenRouter*"
|
|
111
|
+
gh api \
|
|
112
|
+
"repos/${{ github.repository }}/pulls/${PR_NUMBER}/reviews" \
|
|
113
|
+
-f body="$FALLBACK_BODY" \
|
|
114
|
+
-f event="$EVENT" \
|
|
115
|
+
-f commit_id="$HEAD_SHA"
|
|
116
|
+
fi
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
- name: Post full review output as comment
|
|
120
|
+
if: always()
|
|
121
|
+
run: |
|
|
122
|
+
PR_NUMBER=${{ steps.pr.outputs.number }}
|
|
123
|
+
{
|
|
124
|
+
echo "## Claude Code Review Output"
|
|
125
|
+
echo ""
|
|
126
|
+
if [ -f /tmp/review.json ] && jq empty /tmp/review.json 2>/dev/null; then
|
|
127
|
+
VERDICT=$(jq -r '.verdict // "N/A"' /tmp/review.json)
|
|
128
|
+
SUMMARY=$(jq -r '.summary // "N/A"' /tmp/review.json)
|
|
129
|
+
echo "**Verdict:** ${VERDICT}"
|
|
130
|
+
echo ""
|
|
131
|
+
echo "**Summary:** ${SUMMARY}"
|
|
132
|
+
HAS_COMMENTS=$(jq 'has("inline_comments") and (.inline_comments | length > 0)' /tmp/review.json)
|
|
133
|
+
if [ "$HAS_COMMENTS" = "true" ]; then
|
|
134
|
+
echo ""
|
|
135
|
+
echo "### Inline Comments"
|
|
136
|
+
echo ""
|
|
137
|
+
jq -r '.inline_comments[] | "#### \(.file):\(.line)\n\(.comment)\n"' /tmp/review.json
|
|
138
|
+
fi
|
|
139
|
+
else
|
|
140
|
+
echo "*No review output produced or failed to parse.*"
|
|
141
|
+
fi
|
|
142
|
+
echo ""
|
|
143
|
+
echo "---"
|
|
144
|
+
echo "*Automated review by Claude Code via OpenRouter*"
|
|
145
|
+
} > /tmp/comment-body.md
|
|
146
|
+
gh pr comment "$PR_NUMBER" --body-file /tmp/comment-body.md
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Validate Tree
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- "**/*.md"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
validate:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: "22"
|
|
17
|
+
|
|
18
|
+
- name: Validate tree
|
|
19
|
+
run: npx -p first-tree context-tree verify
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const HELP_USAGE = `usage: context-tree help <topic>
|
|
2
|
+
|
|
3
|
+
Topics:
|
|
4
|
+
onboarding How to set up a context tree from scratch
|
|
5
|
+
`;
|
|
6
|
+
|
|
7
|
+
export { HELP_USAGE };
|
|
8
|
+
|
|
9
|
+
type Output = (text: string) => void;
|
|
10
|
+
|
|
11
|
+
export async function runHelp(
|
|
12
|
+
args: string[],
|
|
13
|
+
output: Output = console.log,
|
|
14
|
+
): Promise<number> {
|
|
15
|
+
const topic = args[0];
|
|
16
|
+
|
|
17
|
+
if (!topic || topic === "--help" || topic === "-h") {
|
|
18
|
+
output(HELP_USAGE);
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (topic) {
|
|
23
|
+
case "onboarding": {
|
|
24
|
+
const { runOnboarding } = await import("#skill/engine/onboarding.js");
|
|
25
|
+
return runOnboarding(output);
|
|
26
|
+
}
|
|
27
|
+
default:
|
|
28
|
+
output(`Unknown help topic: ${topic}`);
|
|
29
|
+
output(HELP_USAGE);
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "#skill/engine/init.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "#skill/engine/upgrade.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "#skill/engine/verify.js";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
} from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { Repo } from "#skill/engine/repo.js";
|
|
8
|
+
import { ONBOARDING_TEXT } from "#skill/engine/onboarding.js";
|
|
9
|
+
import { evaluateAll } from "#skill/engine/rules/index.js";
|
|
10
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
11
|
+
import {
|
|
12
|
+
copyCanonicalSkill,
|
|
13
|
+
renderTemplateFile,
|
|
14
|
+
resolveBundledPackageRoot,
|
|
15
|
+
} from "#skill/engine/runtime/installer.js";
|
|
16
|
+
import {
|
|
17
|
+
FRAMEWORK_ASSET_ROOT,
|
|
18
|
+
FRAMEWORK_VERSION,
|
|
19
|
+
INSTALLED_PROGRESS,
|
|
20
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The interactive prompt tool the agent should use to present choices.
|
|
24
|
+
* Different agents may name this differently — change it here to update
|
|
25
|
+
* all generated task text at once.
|
|
26
|
+
*/
|
|
27
|
+
export const INTERACTIVE_TOOL = "AskUserQuestion";
|
|
28
|
+
|
|
29
|
+
const TEMPLATE_MAP: [string, string][] = [
|
|
30
|
+
["root-node.md.template", "NODE.md"],
|
|
31
|
+
["agent.md.template", "AGENT.md"],
|
|
32
|
+
["members-domain.md.template", "members/NODE.md"],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function installSkill(source: string, target: string): void {
|
|
36
|
+
copyCanonicalSkill(source, target);
|
|
37
|
+
console.log(
|
|
38
|
+
" Installed skills/first-tree/ from the bundled first-tree package",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderTemplates(target: string): void {
|
|
43
|
+
const frameworkDir = join(target, FRAMEWORK_ASSET_ROOT);
|
|
44
|
+
for (const [templateName, targetPath] of TEMPLATE_MAP) {
|
|
45
|
+
if (existsSync(join(target, targetPath))) {
|
|
46
|
+
console.log(` Skipped ${targetPath} (already exists)`);
|
|
47
|
+
} else if (renderTemplateFile(frameworkDir, templateName, target, targetPath)) {
|
|
48
|
+
console.log(` Created ${targetPath}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatTaskList(groups: RuleResult[]): string {
|
|
54
|
+
const lines: string[] = [
|
|
55
|
+
"# Context Tree Init\n",
|
|
56
|
+
"**Agent instructions:** Before starting work, analyze the full task list below and" +
|
|
57
|
+
" identify all information you need from the user. Ask the user for their code" +
|
|
58
|
+
" repositories or project directories so you can analyze the source yourself —" +
|
|
59
|
+
" derive project descriptions, domains, and members from the code instead of" +
|
|
60
|
+
" asking the user to describe them. Collect everything upfront using the" +
|
|
61
|
+
` **${INTERACTIVE_TOOL}** tool with structured options — present selectable choices` +
|
|
62
|
+
" (with label and description) so the user can pick instead of typing free-form" +
|
|
63
|
+
` answers. You may batch up to 4 questions per ${INTERACTIVE_TOOL} call.\n`,
|
|
64
|
+
];
|
|
65
|
+
for (const group of groups) {
|
|
66
|
+
lines.push(`## ${group.group}`);
|
|
67
|
+
for (const task of group.tasks) {
|
|
68
|
+
lines.push(`- [ ] ${task}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push("");
|
|
71
|
+
}
|
|
72
|
+
lines.push("## Verification");
|
|
73
|
+
lines.push(
|
|
74
|
+
"After completing the tasks above, run `context-tree verify` to confirm:",
|
|
75
|
+
);
|
|
76
|
+
lines.push(`- [ ] \`${FRAMEWORK_VERSION}\` exists`);
|
|
77
|
+
lines.push("- [ ] Root NODE.md has valid frontmatter (title, owners)");
|
|
78
|
+
lines.push("- [ ] AGENT.md exists with framework markers");
|
|
79
|
+
lines.push("- [ ] `context-tree verify` passes with no errors");
|
|
80
|
+
lines.push("- [ ] At least one member node exists");
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push("---");
|
|
83
|
+
lines.push("");
|
|
84
|
+
lines.push(
|
|
85
|
+
"**Important:** As you complete each task, check it off in" +
|
|
86
|
+
` \`${INSTALLED_PROGRESS}\` by changing \`- [ ]\` to \`- [x]\`.` +
|
|
87
|
+
" Run `context-tree verify` when done — it will fail if any" +
|
|
88
|
+
" items remain unchecked.",
|
|
89
|
+
);
|
|
90
|
+
lines.push("");
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function writeProgress(repo: Repo, content: string): void {
|
|
95
|
+
const progressPath = join(repo.root, repo.preferredProgressPath());
|
|
96
|
+
mkdirSync(dirname(progressPath), { recursive: true });
|
|
97
|
+
writeFileSync(progressPath, content);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface InitOptions {
|
|
101
|
+
sourceRoot?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function runInit(repo?: Repo, options?: InitOptions): number {
|
|
105
|
+
const r = repo ?? new Repo();
|
|
106
|
+
|
|
107
|
+
if (!r.isGitRepo()) {
|
|
108
|
+
console.error(
|
|
109
|
+
"Error: not a git repository. Initialize one first:\n git init",
|
|
110
|
+
);
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!r.hasFramework()) {
|
|
115
|
+
try {
|
|
116
|
+
const sourceRoot = options?.sourceRoot ?? resolveBundledPackageRoot();
|
|
117
|
+
console.log(
|
|
118
|
+
"Installing the framework skill bundled with this first-tree package...",
|
|
119
|
+
);
|
|
120
|
+
console.log("Installing skill and scaffolding...");
|
|
121
|
+
installSkill(sourceRoot, r.root);
|
|
122
|
+
renderTemplates(r.root);
|
|
123
|
+
console.log();
|
|
124
|
+
} catch (err) {
|
|
125
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
126
|
+
console.error(`Error: ${message}`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(ONBOARDING_TEXT);
|
|
132
|
+
console.log("---\n");
|
|
133
|
+
|
|
134
|
+
const groups = evaluateAll(r);
|
|
135
|
+
if (groups.length === 0) {
|
|
136
|
+
console.log("All checks passed. Your context tree is set up.");
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const output = formatTaskList(groups);
|
|
141
|
+
console.log(output);
|
|
142
|
+
writeProgress(r, output);
|
|
143
|
+
console.log(`Progress file written to ${r.preferredProgressPath()}`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
FRAMEWORK_VERSION,
|
|
5
|
+
LEGACY_SKILL_PROGRESS,
|
|
6
|
+
LEGACY_SKILL_VERSION,
|
|
7
|
+
LEGACY_PROGRESS,
|
|
8
|
+
LEGACY_VERSION,
|
|
9
|
+
INSTALLED_PROGRESS,
|
|
10
|
+
type FrameworkLayout,
|
|
11
|
+
detectFrameworkLayout,
|
|
12
|
+
frameworkVersionCandidates,
|
|
13
|
+
progressFileCandidates,
|
|
14
|
+
resolveFirstExistingPath,
|
|
15
|
+
} from "#skill/engine/runtime/asset-loader.js";
|
|
16
|
+
|
|
17
|
+
const FRONTMATTER_RE = /^---\s*\n(.*?)\n---/s;
|
|
18
|
+
const OWNERS_RE = /^owners:\s*\[([^\]]*)\]/m;
|
|
19
|
+
const TITLE_RE = /^title:\s*['"]?(.+?)['"]?\s*$/m;
|
|
20
|
+
|
|
21
|
+
export const FRAMEWORK_BEGIN_MARKER = "<!-- BEGIN CONTEXT-TREE FRAMEWORK";
|
|
22
|
+
export const FRAMEWORK_END_MARKER = "<!-- END CONTEXT-TREE FRAMEWORK -->";
|
|
23
|
+
|
|
24
|
+
export interface Frontmatter {
|
|
25
|
+
title?: string;
|
|
26
|
+
owners?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Repo {
|
|
30
|
+
readonly root: string;
|
|
31
|
+
|
|
32
|
+
constructor(root?: string) {
|
|
33
|
+
this.root = resolve(root ?? process.cwd());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pathExists(relPath: string): boolean {
|
|
37
|
+
return existsSync(join(this.root, relPath));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fileContains(relPath: string, text: string): boolean {
|
|
41
|
+
const fullPath = join(this.root, relPath);
|
|
42
|
+
try {
|
|
43
|
+
const stat = statSync(fullPath);
|
|
44
|
+
if (!stat.isFile()) return false;
|
|
45
|
+
return readFileSync(fullPath, "utf-8").includes(text);
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
readFile(relPath: string): string | null {
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(join(this.root, relPath), "utf-8");
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
frontmatter(relPath: string): Frontmatter | null {
|
|
60
|
+
const text = this.readFile(relPath);
|
|
61
|
+
if (text === null) return null;
|
|
62
|
+
const m = text.match(FRONTMATTER_RE);
|
|
63
|
+
if (!m) return null;
|
|
64
|
+
const fm = m[1];
|
|
65
|
+
const result: Frontmatter = {};
|
|
66
|
+
const titleM = fm.match(TITLE_RE);
|
|
67
|
+
if (titleM) {
|
|
68
|
+
result.title = titleM[1].trim();
|
|
69
|
+
}
|
|
70
|
+
const ownersM = fm.match(OWNERS_RE);
|
|
71
|
+
if (ownersM) {
|
|
72
|
+
const raw = ownersM[1].trim();
|
|
73
|
+
result.owners = raw
|
|
74
|
+
? raw.split(",").map((o) => o.trim()).filter(Boolean)
|
|
75
|
+
: [];
|
|
76
|
+
}
|
|
77
|
+
return result.title !== undefined || result.owners !== undefined
|
|
78
|
+
? result
|
|
79
|
+
: null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
anyAgentConfig(): boolean {
|
|
83
|
+
const knownConfigs = [".claude/settings.json", ".codex/config.json"];
|
|
84
|
+
return knownConfigs.some((c) => this.pathExists(c));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
isGitRepo(): boolean {
|
|
88
|
+
try {
|
|
89
|
+
return statSync(join(this.root, ".git")).isDirectory();
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
hasFramework(): boolean {
|
|
96
|
+
return this.frameworkLayout() !== null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
frameworkLayout(): FrameworkLayout | null {
|
|
100
|
+
return detectFrameworkLayout(this.root);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readVersion(): string | null {
|
|
104
|
+
const versionPath = resolveFirstExistingPath(
|
|
105
|
+
this.root,
|
|
106
|
+
frameworkVersionCandidates(),
|
|
107
|
+
);
|
|
108
|
+
if (versionPath === null) return null;
|
|
109
|
+
const text = this.readFile(versionPath);
|
|
110
|
+
return text ? text.trim() : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
progressPath(): string | null {
|
|
114
|
+
return resolveFirstExistingPath(this.root, progressFileCandidates());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
preferredProgressPath(): string {
|
|
118
|
+
const layout = this.frameworkLayout();
|
|
119
|
+
if (layout === "legacy") {
|
|
120
|
+
return LEGACY_PROGRESS;
|
|
121
|
+
}
|
|
122
|
+
if (layout === "legacy-skill") {
|
|
123
|
+
return LEGACY_SKILL_PROGRESS;
|
|
124
|
+
}
|
|
125
|
+
return INSTALLED_PROGRESS;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
frameworkVersionPath(): string {
|
|
129
|
+
const layout = this.frameworkLayout();
|
|
130
|
+
if (layout === "legacy") {
|
|
131
|
+
return LEGACY_VERSION;
|
|
132
|
+
}
|
|
133
|
+
if (layout === "legacy-skill") {
|
|
134
|
+
return LEGACY_SKILL_VERSION;
|
|
135
|
+
}
|
|
136
|
+
return FRAMEWORK_VERSION;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
hasAgentMdMarkers(): boolean {
|
|
140
|
+
const text = this.readFile("AGENT.md");
|
|
141
|
+
if (text === null) return false;
|
|
142
|
+
return text.includes(FRAMEWORK_BEGIN_MARKER) && text.includes(FRAMEWORK_END_MARKER);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
hasMembers(): boolean {
|
|
146
|
+
const membersDir = join(this.root, "members");
|
|
147
|
+
try {
|
|
148
|
+
if (!statSync(membersDir).isDirectory()) return false;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return existsSync(join(membersDir, "NODE.md"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
memberCount(): number {
|
|
156
|
+
const membersDir = join(this.root, "members");
|
|
157
|
+
try {
|
|
158
|
+
if (!statSync(membersDir).isDirectory()) return 0;
|
|
159
|
+
} catch {
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
let count = 0;
|
|
163
|
+
const walk = (dir: string): void => {
|
|
164
|
+
for (const entry of readdirSync(dir)) {
|
|
165
|
+
const childPath = join(dir, entry);
|
|
166
|
+
try {
|
|
167
|
+
if (!statSync(childPath).isDirectory()) continue;
|
|
168
|
+
} catch {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (existsSync(join(childPath, "NODE.md"))) {
|
|
172
|
+
count++;
|
|
173
|
+
}
|
|
174
|
+
walk(childPath);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
walk(membersDir);
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hasPlaceholderNode(): boolean {
|
|
182
|
+
return this.fileContains("NODE.md", "<!-- PLACEHOLDER");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { FRAMEWORK_END_MARKER } from "#skill/engine/repo.js";
|
|
2
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
3
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
4
|
+
import { FRAMEWORK_TEMPLATES_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
5
|
+
|
|
6
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
7
|
+
const tasks: string[] = [];
|
|
8
|
+
if (!repo.pathExists("AGENT.md")) {
|
|
9
|
+
tasks.push(
|
|
10
|
+
`AGENT.md is missing — create from \`${FRAMEWORK_TEMPLATES_DIR}/agent.md.template\``,
|
|
11
|
+
);
|
|
12
|
+
} else if (!repo.hasAgentMdMarkers()) {
|
|
13
|
+
tasks.push(
|
|
14
|
+
"AGENT.md exists but is missing framework markers — add `<!-- BEGIN CONTEXT-TREE FRAMEWORK -->` and `<!-- END CONTEXT-TREE FRAMEWORK -->` sections",
|
|
15
|
+
);
|
|
16
|
+
} else {
|
|
17
|
+
const text = repo.readFile("AGENT.md") ?? "";
|
|
18
|
+
const afterMarker = text.split(FRAMEWORK_END_MARKER);
|
|
19
|
+
if (afterMarker.length > 1) {
|
|
20
|
+
const userSection = afterMarker[1].trim();
|
|
21
|
+
const lines = userSection
|
|
22
|
+
.split("\n")
|
|
23
|
+
.filter(
|
|
24
|
+
(l) =>
|
|
25
|
+
l.trim() &&
|
|
26
|
+
!l.trim().startsWith("#") &&
|
|
27
|
+
!l.trim().startsWith("<!--"),
|
|
28
|
+
);
|
|
29
|
+
if (lines.length === 0) {
|
|
30
|
+
tasks.push(
|
|
31
|
+
"Add your project-specific instructions below the framework markers in AGENT.md",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { group: "Agent Instructions", order: 3, tasks };
|
|
37
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
2
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
3
|
+
import { FRAMEWORK_EXAMPLES_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
4
|
+
|
|
5
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
6
|
+
const tasks: string[] = [];
|
|
7
|
+
if (repo.pathExists(".claude/settings.json")) {
|
|
8
|
+
if (!repo.fileContains(".claude/settings.json", "inject-tree-context")) {
|
|
9
|
+
tasks.push(
|
|
10
|
+
`Add SessionStart hook to \`.claude/settings.json\` (see \`${FRAMEWORK_EXAMPLES_DIR}/claude-code/\`)`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
} else if (!repo.anyAgentConfig()) {
|
|
14
|
+
tasks.push(
|
|
15
|
+
`No agent configuration detected. Configure your agent to load tree context at session start. See \`${FRAMEWORK_EXAMPLES_DIR}/\` for supported agents. You can skip this and set it up later.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return { group: "Agent Integration", order: 5, tasks };
|
|
19
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { INTERACTIVE_TOOL } from "#skill/engine/init.js";
|
|
4
|
+
import type { Repo } from "#skill/engine/repo.js";
|
|
5
|
+
import type { RuleResult } from "#skill/engine/rules/index.js";
|
|
6
|
+
import { FRAMEWORK_WORKFLOWS_DIR } from "#skill/engine/runtime/asset-loader.js";
|
|
7
|
+
|
|
8
|
+
export function evaluate(repo: Repo): RuleResult {
|
|
9
|
+
const tasks: string[] = [];
|
|
10
|
+
let hasValidation = false;
|
|
11
|
+
let hasPrReview = false;
|
|
12
|
+
let hasCodeowners = false;
|
|
13
|
+
const workflowsDir = join(repo.root, ".github", "workflows");
|
|
14
|
+
try {
|
|
15
|
+
if (statSync(workflowsDir).isDirectory()) {
|
|
16
|
+
for (const name of readdirSync(workflowsDir)) {
|
|
17
|
+
if (!name.endsWith(".yml") && !name.endsWith(".yaml")) continue;
|
|
18
|
+
const fullPath = join(workflowsDir, name);
|
|
19
|
+
try {
|
|
20
|
+
if (!statSync(fullPath).isFile()) continue;
|
|
21
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
22
|
+
if (
|
|
23
|
+
content.includes("validate_nodes") ||
|
|
24
|
+
content.includes("validate_members")
|
|
25
|
+
) {
|
|
26
|
+
hasValidation = true;
|
|
27
|
+
}
|
|
28
|
+
if (content.includes("run-review")) {
|
|
29
|
+
hasPrReview = true;
|
|
30
|
+
}
|
|
31
|
+
if (content.includes("generate-codeowners")) {
|
|
32
|
+
hasCodeowners = true;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// workflows dir doesn't exist
|
|
41
|
+
}
|
|
42
|
+
if (!hasValidation) {
|
|
43
|
+
tasks.push(
|
|
44
|
+
`No validation workflow found — copy \`${FRAMEWORK_WORKFLOWS_DIR}/validate.yml\` to \`.github/workflows/validate.yml\``,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (!hasPrReview) {
|
|
48
|
+
tasks.push(
|
|
49
|
+
`Use ${INTERACTIVE_TOOL} to ask whether the user wants AI-powered PR reviews. Options:\n` +
|
|
50
|
+
" 1. **OpenRouter** — use an OpenRouter API key\n" +
|
|
51
|
+
" 2. **Claude API** — use a Claude API key directly\n" +
|
|
52
|
+
" 3. **Skip** — do not set up PR reviews\n" +
|
|
53
|
+
`If (1): copy \`${FRAMEWORK_WORKFLOWS_DIR}/pr-review.yml\` to \`.github/workflows/pr-review.yml\` as-is; the repo secret name is \`OPENROUTER_API_KEY\`. ` +
|
|
54
|
+
"If (2): copy the workflow and replace the `env` block with `ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}`, remove the `ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, and `ANTHROPIC_DEFAULT_SONNET_MODEL` lines; the repo secret name is `ANTHROPIC_API_KEY`. " +
|
|
55
|
+
"If (3): skip this and the next task.",
|
|
56
|
+
);
|
|
57
|
+
tasks.push(
|
|
58
|
+
`Use ${INTERACTIVE_TOOL} to ask how the user wants to configure the API secret. Options:\n` +
|
|
59
|
+
" 1. **Set it now** — provide the key and the agent will run `gh secret set <SECRET_NAME> --body <KEY>`\n" +
|
|
60
|
+
" 2. **I'll do it myself** — the agent will show manual instructions\n" +
|
|
61
|
+
"If (1): ask the user to provide the key, then run `gh secret set` with the secret name from the previous step. " +
|
|
62
|
+
"If (2): tell the user to go to their repo → Settings → Secrets and variables → Actions → New repository secret, and create the secret with the name from the previous step. " +
|
|
63
|
+
"Skip this task if the user chose Skip in the previous step.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!hasCodeowners) {
|
|
67
|
+
tasks.push(
|
|
68
|
+
`No CODEOWNERS workflow found — copy \`${FRAMEWORK_WORKFLOWS_DIR}/codeowners.yml\` to \`.github/workflows/codeowners.yml\` to auto-generate CODEOWNERS from tree ownership on every PR.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return { group: "CI / Validation", order: 6, tasks };
|
|
72
|
+
}
|