boomtick-cli 0.2.1__py3-none-any.whl
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.
- boomtick_cli-0.2.1.dist-info/METADATA +20 -0
- boomtick_cli-0.2.1.dist-info/RECORD +103 -0
- boomtick_cli-0.2.1.dist-info/WHEEL +5 -0
- boomtick_cli-0.2.1.dist-info/entry_points.txt +3 -0
- boomtick_cli-0.2.1.dist-info/top_level.txt +1 -0
- dev_tools/__init__.py +1 -0
- dev_tools/cli-schema.json +2028 -0
- dev_tools/cli.py +1542 -0
- dev_tools/config.py +235 -0
- dev_tools/constants.py +14 -0
- dev_tools/daemon.py +146 -0
- dev_tools/dist/config.js +108 -0
- dev_tools/dist/index.js +10 -0
- dev_tools/dist/lib/error_utils.js +8 -0
- dev_tools/dist/lib/git.js +31 -0
- dev_tools/dist/lib/result.js +21 -0
- dev_tools/dist/lib/shell.js +91 -0
- dev_tools/dist/lib/shell.test.js +10 -0
- dev_tools/dist/lib/td-cli.js +27 -0
- dev_tools/dist/lib/test-utils.js +20 -0
- dev_tools/dist/mcp/definitions.js +242 -0
- dev_tools/dist/mcp/server.js +317 -0
- dev_tools/dist/mcp/tools.js +18 -0
- dev_tools/dist/tools/contract.js +2069 -0
- dev_tools/dist/tools/ddgs.search.js +50 -0
- dev_tools/dist/tools/ddgs.search.test.js +67 -0
- dev_tools/dist/tools/ddgs_search.py +23 -0
- dev_tools/dist/tools/github.checkout_branch.js +20 -0
- dev_tools/dist/tools/github.comment_triage_summary.js +25 -0
- dev_tools/dist/tools/github.create_issue.js +28 -0
- dev_tools/dist/tools/github.create_issue.test.js +54 -0
- dev_tools/dist/tools/github.create_pull_request.js +44 -0
- dev_tools/dist/tools/github.get_merge_conflict_files.js +26 -0
- dev_tools/dist/tools/github.get_pr.js +18 -0
- dev_tools/dist/tools/github.get_pr_diff.js +24 -0
- dev_tools/dist/tools/github.issue_comment.js +24 -0
- dev_tools/dist/tools/github.issue_update.js +29 -0
- dev_tools/dist/tools/github.issue_view.js +28 -0
- dev_tools/dist/tools/github.open_replacement_pr.js +41 -0
- dev_tools/dist/tools/github.search_open_prs.js +31 -0
- dev_tools/dist/tools/github.search_open_prs.test.js +51 -0
- dev_tools/dist/tools/jules/cancel-session.js +20 -0
- dev_tools/dist/tools/jules/create-session.js +20 -0
- dev_tools/dist/tools/jules/create-session.test.js +99 -0
- dev_tools/dist/tools/jules/get-messages.js +19 -0
- dev_tools/dist/tools/jules/get-messages.test.js +45 -0
- dev_tools/dist/tools/jules/get-pr.js +14 -0
- dev_tools/dist/tools/jules/get-pr.test.js +37 -0
- dev_tools/dist/tools/jules/get-session.js +10 -0
- dev_tools/dist/tools/jules/list-sessions.js +43 -0
- dev_tools/dist/tools/jules/send-message.js +22 -0
- dev_tools/dist/tools/jules/send-message.test.js +56 -0
- dev_tools/dist/tools/jules/shared.js +26 -0
- dev_tools/dist/tools/jules/trigger-feedback.js +16 -0
- dev_tools/dist/tools/jules/trigger-feedback.test.js +42 -0
- dev_tools/dist/tools/repo.commit_patch.js +33 -0
- dev_tools/dist/tools/repo.create_branch.js +21 -0
- dev_tools/dist/tools/repo.create_branch.test.js +42 -0
- dev_tools/dist/tools/repo.create_repair_branch.js +38 -0
- dev_tools/dist/tools/repo.get_changed_files.js +21 -0
- dev_tools/dist/tools/repo.get_command_schema.js +27 -0
- dev_tools/dist/tools/repo.get_package_scripts.js +35 -0
- dev_tools/dist/tools/repo.get_package_scripts.test.js +39 -0
- dev_tools/dist/tools/repo.get_route_map.js +37 -0
- dev_tools/dist/tools/repo.logs.js +16 -0
- dev_tools/dist/tools/repo.read_agent_context.js +20 -0
- dev_tools/dist/tools/repo.read_ci_logs.js +18 -0
- dev_tools/dist/tools/repo.run_lighthouse.js +37 -0
- dev_tools/dist/tools/repo.run_playwright.js +29 -0
- dev_tools/dist/tools/repo.run_tests.js +43 -0
- dev_tools/dist/tools/types.js +1 -0
- dev_tools/get_ai_context.py +63 -0
- dev_tools/handlers/__init__.py +1 -0
- dev_tools/handlers/command_handler.py +69 -0
- dev_tools/mcp_server.py +36 -0
- dev_tools/models.py +354 -0
- dev_tools/orchestrator.py +2841 -0
- dev_tools/pr_overlap.py +139 -0
- dev_tools/resources/__init__.py +1 -0
- dev_tools/resources/build-repo-context.py +182 -0
- dev_tools/resources/prompt_constants.json +5 -0
- dev_tools/resources/review_template.md +41 -0
- dev_tools/resources/ux-audit.config.json +15 -0
- dev_tools/resources/visual_guidelines.json +3 -0
- dev_tools/review_read_pass.py +232 -0
- dev_tools/schema_gen.py +55 -0
- dev_tools/schema_utils.py +125 -0
- dev_tools/scope_check.py +85 -0
- dev_tools/services/__init__.py +1 -0
- dev_tools/services/ai_service.py +816 -0
- dev_tools/services/dependency_graph.py +123 -0
- dev_tools/services/github.py +783 -0
- dev_tools/services/jules.py +181 -0
- dev_tools/services/repair_service.py +199 -0
- dev_tools/services/vector_store.py +82 -0
- dev_tools/services/vision_service.py +91 -0
- dev_tools/td_cli.py +28 -0
- dev_tools/utils/__init__.py +1035 -0
- dev_tools/utils/git.py +68 -0
- dev_tools/ux_report.py +213 -0
- dev_tools/verify_infra.py +91 -0
- dev_tools/verify_versions.py +191 -0
- dev_tools/version_utils.py +175 -0
dev_tools/pr_overlap.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# pylint: disable=invalid-name,line-too-long,missing-docstring,no-value-for-parameter
|
|
2
|
+
import sys
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from dev_tools.services.github import GitHubClient
|
|
8
|
+
from dev_tools.utils import log_error, log_info
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_pr_overlaps(github: GitHubClient, limit: int) -> List[Dict[str, Any]]:
|
|
12
|
+
"""
|
|
13
|
+
Identifies structural overlaps between open Pull Requests by analyzing modified file sets.
|
|
14
|
+
|
|
15
|
+
This function fetches open PRs from the repository, retrieves the list of modified files
|
|
16
|
+
for each PR, and identifies groups (clusters) of PRs that share at least one common file.
|
|
17
|
+
These clusters are useful for identifying potential merge conflicts or functional
|
|
18
|
+
dependencies that might necessitate PR consolidation.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
github: An instance of GitHubClient to interact with the GitHub API.
|
|
22
|
+
limit: The maximum number of open PRs to retrieve for analysis.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A list of cluster dictionaries, each containing:
|
|
26
|
+
- 'prs': A list of PR numbers in the cluster.
|
|
27
|
+
- 'files': A list of filenames shared across the PRs in the cluster.
|
|
28
|
+
- 'metadata': A dictionary mapping PR numbers to their full PR details.
|
|
29
|
+
"""
|
|
30
|
+
# 1. Fetch open PRs
|
|
31
|
+
log_info(f"Fetching up to {limit} open PRs...")
|
|
32
|
+
prs = github.list_pull_requests(state="open", limit=limit)
|
|
33
|
+
|
|
34
|
+
if not prs:
|
|
35
|
+
log_info("No open PRs found.")
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
# 2. Map files to PRs
|
|
39
|
+
file_to_prs = defaultdict(set)
|
|
40
|
+
pr_metadata = {}
|
|
41
|
+
|
|
42
|
+
for pr in prs:
|
|
43
|
+
pr_num = pr["number"]
|
|
44
|
+
pr_metadata[pr_num] = pr
|
|
45
|
+
log_info(f"Fetching files for PR #{pr_num}...")
|
|
46
|
+
try:
|
|
47
|
+
files = github.fetch_pr_files(pr_num)
|
|
48
|
+
for f in files:
|
|
49
|
+
filename = f.get("filename")
|
|
50
|
+
if filename:
|
|
51
|
+
file_to_prs[filename].add(pr_num)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
log_error(f"Failed to fetch files for PR #{pr_num}: {e}")
|
|
54
|
+
|
|
55
|
+
# 3. Identify clusters
|
|
56
|
+
# A cluster is a set of PRs that share at least one file.
|
|
57
|
+
# We use a simple disjoint-set or just group by overlap.
|
|
58
|
+
# For PR consolidation, we want to see pairs/groups that have overlaps.
|
|
59
|
+
|
|
60
|
+
overlaps = defaultdict(set)
|
|
61
|
+
for filename, pr_set in file_to_prs.items():
|
|
62
|
+
if len(pr_set) > 1:
|
|
63
|
+
sorted_prs = tuple(sorted(list(pr_set)))
|
|
64
|
+
overlaps[sorted_prs].add(filename)
|
|
65
|
+
|
|
66
|
+
clusters = []
|
|
67
|
+
for pr_set, files in overlaps.items():
|
|
68
|
+
clusters.append({
|
|
69
|
+
"prs": list(pr_set),
|
|
70
|
+
"files": sorted(list(files)),
|
|
71
|
+
"metadata": {num: pr_metadata[num] for num in pr_set}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
# Sort clusters by number of overlapping files (descending)
|
|
75
|
+
clusters.sort(key=lambda x: len(x["files"]), reverse=True)
|
|
76
|
+
return clusters
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def report_overlaps(clusters: List[Dict[str, Any]]):
|
|
80
|
+
"""
|
|
81
|
+
Generates and prints a Markdown-formatted report of identified PR overlaps to stdout.
|
|
82
|
+
|
|
83
|
+
The report includes details for each cluster, such as the involved PR numbers,
|
|
84
|
+
the author of each PR, and a list of the primary overlapping files. It also
|
|
85
|
+
provides actionable recommendations for consolidation or coordination.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
clusters: A list of cluster dictionaries as returned by `get_pr_overlaps`.
|
|
89
|
+
"""
|
|
90
|
+
if not clusters:
|
|
91
|
+
print("✅ No significant PR overlaps detected.")
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
print("# Active PR Overlap Analysis")
|
|
95
|
+
print("Identified clusters of PRs with shared file modifications.\n")
|
|
96
|
+
|
|
97
|
+
for i, cluster in enumerate(clusters, 1):
|
|
98
|
+
pr_list = cluster["prs"]
|
|
99
|
+
files = cluster["files"]
|
|
100
|
+
metadata = cluster["metadata"]
|
|
101
|
+
|
|
102
|
+
print(f"## Cluster {i}: PRs {', '.join(f'#{num}' for num in pr_list)}")
|
|
103
|
+
print(f"**Primary Overlap:** {len(files)} files")
|
|
104
|
+
|
|
105
|
+
# List top 10 files
|
|
106
|
+
for f in files[:10]:
|
|
107
|
+
print(f"- `{f}`")
|
|
108
|
+
if len(files) > 10:
|
|
109
|
+
print(f"- ... and {len(files) - 10} more")
|
|
110
|
+
|
|
111
|
+
print("\n**Involved PRs:**")
|
|
112
|
+
for num in pr_list:
|
|
113
|
+
pr = metadata[num]
|
|
114
|
+
print(f"- **#{num}**: {pr.get('title')} (Author: @{pr.get('author', {}).get('login', 'unknown')})")
|
|
115
|
+
|
|
116
|
+
print("\n**Recommendation:** Merge/Coordinate")
|
|
117
|
+
print("**Rationale:** High file overlap suggests these PRs may have functional dependencies or cause merge conflicts.")
|
|
118
|
+
print("\n---\n")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@click.command()
|
|
122
|
+
@click.option("--limit", default=20, help="Limit the number of PRs to process")
|
|
123
|
+
@click.option("--no-cache", is_flag=True, help="Bypass the disk cache for GitHub API calls")
|
|
124
|
+
def run_cli(limit, no_cache):
|
|
125
|
+
try:
|
|
126
|
+
github = GitHubClient(no_cache=no_cache)
|
|
127
|
+
clusters = get_pr_overlaps(github, limit)
|
|
128
|
+
report_overlaps(clusters)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
log_error(f"Overlap analysis failed: {e}")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
run_cli() # pylint: disable=no-value-for-parameter
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# pylint: disable=missing-docstring
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# pylint: disable=import-outside-toplevel,invalid-name,line-too-long,missing-docstring,too-many-branches,too-many-locals,too-many-statements
|
|
2
|
+
#!/usr/bin/env python3
|
|
3
|
+
import json
|
|
4
|
+
import pathlib
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_repo_context():
|
|
9
|
+
"""Gathers static context about the repository."""
|
|
10
|
+
|
|
11
|
+
# Discovery: find the repo root and package root
|
|
12
|
+
script_path = pathlib.Path(__file__).resolve()
|
|
13
|
+
# boomtick-pkg/scripts/build-repo-context.py -> boomtick-pkg
|
|
14
|
+
package_root = script_path.parent.parent
|
|
15
|
+
|
|
16
|
+
# Check if we are in a monorepo or a standalone repo
|
|
17
|
+
if (package_root.parent / "package.json").exists() and (package_root.parent / "boomtick-pkg").exists():
|
|
18
|
+
# Monorepo mode
|
|
19
|
+
repo_root = package_root.parent
|
|
20
|
+
else:
|
|
21
|
+
# Standalone mode
|
|
22
|
+
repo_root = package_root
|
|
23
|
+
|
|
24
|
+
# If we are running from a location that isn't boomtick-pkg/scripts,
|
|
25
|
+
# fallback to discovery based on workspace.json
|
|
26
|
+
if package_root.name != "boomtick-pkg" or not (package_root / "workspace.json").exists():
|
|
27
|
+
# Search upwards for workspace.json using Path.parents
|
|
28
|
+
found_pkg = False
|
|
29
|
+
for parent in script_path.parents:
|
|
30
|
+
if (parent / "workspace.json").exists():
|
|
31
|
+
package_root = parent
|
|
32
|
+
found_pkg = True
|
|
33
|
+
break
|
|
34
|
+
|
|
35
|
+
if found_pkg:
|
|
36
|
+
if (package_root.parent / "package.json").exists() and (package_root.parent / "boomtick-pkg").exists():
|
|
37
|
+
repo_root = package_root.parent
|
|
38
|
+
else:
|
|
39
|
+
repo_root = package_root
|
|
40
|
+
else:
|
|
41
|
+
# Absolute fallback
|
|
42
|
+
repo_root = pathlib.Path(".").resolve()
|
|
43
|
+
package_root = repo_root if (repo_root / "workspace.json").exists() else repo_root / "boomtick-pkg"
|
|
44
|
+
|
|
45
|
+
# 1. Package JSON (Repo Root)
|
|
46
|
+
try:
|
|
47
|
+
package_json_path = repo_root / "package.json"
|
|
48
|
+
if not package_json_path.exists() and (repo_root / "workspace.json").exists():
|
|
49
|
+
# In extracted standalone mode, use workspace.json as the package authority
|
|
50
|
+
package_json_path = repo_root / "workspace.json"
|
|
51
|
+
|
|
52
|
+
package_json = json.loads(package_json_path.read_text())
|
|
53
|
+
# simplify package.json to the most important parts for context
|
|
54
|
+
package_summary = {
|
|
55
|
+
"name": package_json.get("name"),
|
|
56
|
+
"scripts": package_json.get("scripts", {}),
|
|
57
|
+
"dependencies": sorted(list(package_json.get("dependencies", {}).keys())),
|
|
58
|
+
"devDependencies": sorted(list(package_json.get("devDependencies", {}).keys())),
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
print(f"Error reading package.json: {e}", file=sys.stderr)
|
|
62
|
+
package_summary = {}
|
|
63
|
+
|
|
64
|
+
# 2. Project Config (Repo Root)
|
|
65
|
+
project_config = {}
|
|
66
|
+
try:
|
|
67
|
+
project_config_path = repo_root / "project_config.json"
|
|
68
|
+
if project_config_path.exists():
|
|
69
|
+
project_config = json.loads(project_config_path.read_text())
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"Error reading project_config.json: {e}", file=sys.stderr)
|
|
72
|
+
|
|
73
|
+
# 3. MCP Schema (Package Internal)
|
|
74
|
+
mcp_schema = {"tools": [], "prompts": [], "resources": []}
|
|
75
|
+
try:
|
|
76
|
+
import subprocess
|
|
77
|
+
|
|
78
|
+
mcp_dir = package_root / "mcp"
|
|
79
|
+
if mcp_dir.exists():
|
|
80
|
+
# Use npx tsx to run the export script without needing to compile it
|
|
81
|
+
# We run it from the mcp_dir to ensure it finds its own local config
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
["npx", "tsx", "scripts/export-mcp-schema.ts"],
|
|
84
|
+
cwd=str(mcp_dir),
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
check=True,
|
|
88
|
+
)
|
|
89
|
+
mcp_schema = json.loads(result.stdout)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"Error gathering MCP schema: {e}", file=sys.stderr)
|
|
92
|
+
|
|
93
|
+
# 4. CLI Schema (Package Internal)
|
|
94
|
+
cli_schema = {
|
|
95
|
+
"tool_name": "td-cli",
|
|
96
|
+
"schema_authority": "Use 'repo.get_command_schema' or 'td-cli schema <path>' for granular discovery. This file remains for legacy fallback.",
|
|
97
|
+
"description": "Custom developer CLI for BoomTick repository management.",
|
|
98
|
+
"base_command": "td-cli",
|
|
99
|
+
}
|
|
100
|
+
try:
|
|
101
|
+
from dev_tools.cli import cli
|
|
102
|
+
from dev_tools.schema_utils import collect_commands
|
|
103
|
+
|
|
104
|
+
# We keep cli-schema.json updated but minimal for the aggregate context
|
|
105
|
+
# Increasing max_depth to 2 to allow discovery of nested subcommands (e.g., gh search-prs)
|
|
106
|
+
generated_subcommands = collect_commands(cli, max_depth=2)
|
|
107
|
+
cli_schema["subcommands"] = generated_subcommands
|
|
108
|
+
|
|
109
|
+
cli_schema_path = package_root / "cli" / "dev_tools" / "cli-schema.json"
|
|
110
|
+
|
|
111
|
+
# Write full schema for legacy/reference but don't bloat the agent context
|
|
112
|
+
full_subcommands = collect_commands(cli)
|
|
113
|
+
full_payload = cli_schema.copy()
|
|
114
|
+
full_payload["subcommands"] = full_subcommands
|
|
115
|
+
cli_schema_path.write_text(json.dumps(full_payload, indent=2))
|
|
116
|
+
|
|
117
|
+
# Also trigger Pydantic model contract generation
|
|
118
|
+
try:
|
|
119
|
+
import io
|
|
120
|
+
from contextlib import redirect_stdout
|
|
121
|
+
|
|
122
|
+
from dev_tools.schema_gen import generate_schema
|
|
123
|
+
|
|
124
|
+
# Capture stdout to ensure clean JSON output for build-repo-context.py
|
|
125
|
+
with redirect_stdout(io.StringIO()):
|
|
126
|
+
generate_schema()
|
|
127
|
+
# Note: sync-contracts.ts is run via pnpm run verify:schemas or manually
|
|
128
|
+
except Exception as schema_err:
|
|
129
|
+
print(f"Error generating model schemas: {schema_err}", file=sys.stderr)
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"Error generating cli-schema.json dynamically: {e}", file=sys.stderr)
|
|
133
|
+
# Fallback to reading file if generation failed
|
|
134
|
+
try:
|
|
135
|
+
cli_schema_path = package_root / "cli" / "dev_tools" / "cli-schema.json"
|
|
136
|
+
if cli_schema_path.exists():
|
|
137
|
+
cli_schema = json.loads(cli_schema_path.read_text())
|
|
138
|
+
except Exception as read_err:
|
|
139
|
+
print(f"Fallback read of cli-schema.json failed: {read_err}", file=sys.stderr)
|
|
140
|
+
|
|
141
|
+
# 5. File Tree (Repo Root)
|
|
142
|
+
def get_dir_structure(path, max_depth=2, current_depth=0):
|
|
143
|
+
if current_depth >= max_depth:
|
|
144
|
+
return "..."
|
|
145
|
+
structure = {}
|
|
146
|
+
try:
|
|
147
|
+
for item in sorted(path.iterdir()):
|
|
148
|
+
if item.name.startswith(".") or item.name == "node_modules" or item.name == "__pycache__":
|
|
149
|
+
continue
|
|
150
|
+
if item.is_dir():
|
|
151
|
+
structure[item.name + "/"] = get_dir_structure(item, max_depth, current_depth + 1)
|
|
152
|
+
else:
|
|
153
|
+
structure[item.name] = None
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
return structure
|
|
157
|
+
|
|
158
|
+
file_tree = get_dir_structure(repo_root)
|
|
159
|
+
|
|
160
|
+
# Assemble context
|
|
161
|
+
return {
|
|
162
|
+
"repo": {
|
|
163
|
+
"name": package_summary.get("name", "Unknown Repo"),
|
|
164
|
+
},
|
|
165
|
+
"package_json": package_summary,
|
|
166
|
+
"project_config": project_config,
|
|
167
|
+
"mcp_schema": mcp_schema,
|
|
168
|
+
"cli_schema": cli_schema,
|
|
169
|
+
"file_tree": file_tree,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
try:
|
|
175
|
+
context = build_repo_context()
|
|
176
|
+
if not context.get("package_json"):
|
|
177
|
+
raise ValueError("Failed to gather basic repository context (package.json missing or invalid)")
|
|
178
|
+
# sort_keys=True ensures the output is deterministic for revision control
|
|
179
|
+
print(json.dumps(context, indent=2, sort_keys=True))
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"FATAL: Context generation failed: {e}", file=sys.stderr)
|
|
182
|
+
sys.exit(1)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"STRICT_JSON_VERIFICATION": "Strict JSON Verification:\n- You MUST self-verify the completeness and validity of the JSON block before finishing your response.\n- Every finding MUST have an `id`, `file`, `issue`, and `status`.\n- Ensure the JSON is well-formed and contained entirely within the `<findings>` tags.\n- Ensure 'snippet' is a unique string from the diff that identifies the issue.",
|
|
3
|
+
"SNIPPET_AND_VERIFICATION_RULES": "Snippet and verification rules:\n- STRICT SNIPPET RULE: When citing an error or anti-pattern, you MUST quote the entire, exact line from the diff in the \"snippet\" field. Do not truncate the line.\n- Before flagging a \"syntax error\" or \"missing property/method\", re-read the diff to confirm the code isn't simply continued on the next line or truncated in the diff chunk. Hallucinating errors due to chunk truncation is a severe failure.\n- If a line appears truncated in the diff (e.g. at the edge of a chunk), DO NOT assume it is a syntax error. Assume it is valid code that continues outside the visible context.",
|
|
4
|
+
"COMMON_REVIEW_GUIDELINES": "Review ONLY PR changes. Assume original code worked.\nEVIDENCE RULE: Issue must point to exact line + explain runtime consequence.\nFALSE POSITIVE FILTER: No speculation. Design choices are NOT bugs.\n\nTIERED SCOPE:\n- For App/UI (src/): Flag redundant wrappers. BANNED: Raw Tailwind layout (flex/grid/px-*) in TSX (use Stack/Grid/Box).\n- For Infra/Tooling (scripts/, boomtick-pkg/cli/, .github/): Focus on portability, idempotency, and error handling. Avoid UI-specific feedback for low-level scripts.\n\nREPO RULES: Prefer removal.\nANTI-SLOP: DO NOT recommend overly complex error handling, defensive guards, extra unit tests for simple internal scripts, or boilerplate documentation/comments.\n\n- FILE NECESSITY: Question any added, moved, or removed files that look like temporary artifacts (e.g. .tmp, standalone .py in root, audit-*.md, .json dumps) or seem unrelated to the PR intent. Flag them for removal if they pollute the review context."
|
|
5
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# PR Review: #{pr_num}
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
- **Last Commit Tracked (SHA):** {head_sha}
|
|
6
|
+
|
|
7
|
+
## Audit Checklist
|
|
8
|
+
|
|
9
|
+
For EVERY changed file, verify against these standards. Mark as `- [x]` when verified.
|
|
10
|
+
|
|
11
|
+
- [ ] Dead abstractions: No new class, context, or hook that a simpler primitive handles.
|
|
12
|
+
- [ ] Unnecessary indirection: No layer of wrapping where a direct function call suffices.
|
|
13
|
+
- [ ] Responsibility creep: Component does not take on state/logic belonging in parent/hook.
|
|
14
|
+
- [ ] Import bloat: No unnecessary `import React from 'react'` (React 17+).
|
|
15
|
+
- [ ] Token compliance: Uses established design tokens (no raw Tailwind values or inline styles).
|
|
16
|
+
- [ ] Audit ratio: If > 100 lines added, identified at least 10 lines to refactor/remove.
|
|
17
|
+
|
|
18
|
+
## CI Log Triage
|
|
19
|
+
|
|
20
|
+
(Populated if CI failures detected)
|
|
21
|
+
- **Failed Checks:**
|
|
22
|
+
{failed_checks}
|
|
23
|
+
- **Detected Errors:**
|
|
24
|
+
{detected_errors}
|
|
25
|
+
- **Root Cause Analysis:**
|
|
26
|
+
- **Remediation Steps:**
|
|
27
|
+
|
|
28
|
+
## Output JSON
|
|
29
|
+
|
|
30
|
+
Provide your findings and inline comments in the JSON block below.
|
|
31
|
+
DO NOT REMOVE THE BACKTICKS.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{{
|
|
35
|
+
"recommendation": "Approved",
|
|
36
|
+
"body": "## ANTI-AI-SLOP\\n<findings>\\n\\n## FINDINGS\\n<summary>\\n\\n## FINAL RECOMMENDATION\\n<Approved | Approved with Minor Changes | Not Approved>\\n\\n<!-- td-review-manager-comment -->",
|
|
37
|
+
"recommendation": "<Approved | Approved with Minor Changes | Not Approved>",
|
|
38
|
+
"labels": [],
|
|
39
|
+
"comments": []
|
|
40
|
+
}}
|
|
41
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"baseUrl": "http://localhost:3000",
|
|
3
|
+
"viewports": {
|
|
4
|
+
"desktop": [
|
|
5
|
+
{ "name": "desktop-1280", "width": 1280, "height": 800 },
|
|
6
|
+
{ "name": "desktop-1440", "width": 1440, "height": 900 }
|
|
7
|
+
],
|
|
8
|
+
"mobile": [
|
|
9
|
+
{ "name": "mobile-375", "width": 375, "height": 812 },
|
|
10
|
+
{ "name": "mobile-390", "width": 390, "height": 844 },
|
|
11
|
+
{ "name": "mobile-430", "width": 430, "height": 932 }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"outputDir": "artifacts/ux-audit"
|
|
15
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{
|
|
2
|
+
"VISUAL_DESIGN_GUIDELINES": "### VISUAL & DESIGN GUIDELINES (Impeccable System)\nReviewing agents must audit pull requests against the following visual constraints:\n\nA. Color & Contrast Rules\n- WCAG Contrast: Body text against background must be >= 4.5:1. Large text (>= 18px or bold >= 14px) must be >= 3:1.\n- Theme Anchor: Midnight Luster (#020617 / oklch(10% 0.02 240)) base canvas, with luminous Cyber Cyan (#22d3ee / oklch(75% 0.18 190)) triggers. No saturated cream, sand, or paper backgrounds.\n- Color Strategy: Committed/Drenched accenting (color must feel intentional and carry the section, restricted to <= 10% on primary action areas).\n\nB. Typography Limits\n- Line Length: Max body width is 65–75ch to prevent scanning fatigue.\n- Display Headings: Letter-spacing floor >= -0.04em (prevent touching letters). Ceiling size max <= 6rem.\n- Line Balance: H1–H3 must use text-wrap: balance. Articles/prose must use text-wrap: pretty.\n\nC. Layout & Structure\n- No Card Nesting: Cards inside cards are prohibited.\n- Primitives: Flexbox/grid layouts must use standard abstractions (Stack, Grid, Box). Raw Tailwind layout rules (flex, grid, px-4) are banned in app layers.\n- Z-Index Scale: Semantic scale only (dropdown -> sticky -> modal -> toast). No magic numbers like z-[99999].\n\nD. Motion\n- Layout Animations: Do not animate CSS layout dimensions (width, height, flex) unless necessary (performance bottleneck).\n- Reduced Motion: Every transition must respect @media (prefers-reduced-motion: reduce).\n- Entrance reveals: Must degrade gracefully; never gate essential content visibility behind script-triggered animations.\n\nE. Absolute Bans (Auto-Reject Checklist)\n- Side-Stripe Borders: Banned border-left accents on list items/cards.\n- Gradient Text: Combinations of background-clip: text and gradients are banned.\n- Over-rounded Elements: Radius limit is 12–16px (no 32px+ pill cards).\n- Sketchy SVGs: Hand-drawn, sketchy, or crude illustrations are prohibited.\n- Tracked Kickers: Repeating Kickers/Eyebrows above every section is banned."
|
|
3
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# pylint: disable=import-outside-toplevel,missing-docstring,redefined-outer-name,too-many-locals
|
|
2
|
+
#!/usr/bin/env python3
|
|
3
|
+
"""
|
|
4
|
+
review_read_pass.py – Diff parser and file-chunk splitter for the piecemeal
|
|
5
|
+
AI review pipeline.
|
|
6
|
+
|
|
7
|
+
Key exports
|
|
8
|
+
-----------
|
|
9
|
+
parse_diff_into_file_chunks(diff_text) -> list[dict]
|
|
10
|
+
Groups a unified diff into one chunk per file (or multiple chunks for
|
|
11
|
+
large files) and applies skip rules so non-code assets are never sent
|
|
12
|
+
to the AI reviewer.
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
# ── Skip rules ────────────────────────────────────────────────────────────────
|
|
22
|
+
# Files matching any of these patterns will be classified as skip=True.
|
|
23
|
+
_SKIP_EXTENSIONS = {
|
|
24
|
+
".png",
|
|
25
|
+
".jpg",
|
|
26
|
+
".jpeg",
|
|
27
|
+
".gif",
|
|
28
|
+
".svg",
|
|
29
|
+
".ico",
|
|
30
|
+
".webp",
|
|
31
|
+
".woff",
|
|
32
|
+
".woff2",
|
|
33
|
+
".ttf",
|
|
34
|
+
".eot",
|
|
35
|
+
".otf", # images / fonts
|
|
36
|
+
".mp4",
|
|
37
|
+
".mov",
|
|
38
|
+
".avi",
|
|
39
|
+
".webm", # video
|
|
40
|
+
".pdf",
|
|
41
|
+
".zip",
|
|
42
|
+
".tar",
|
|
43
|
+
".gz",
|
|
44
|
+
".br", # binary archives
|
|
45
|
+
".map", # source maps
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_SKIP_FILENAMES = {
|
|
49
|
+
"package-lock.json",
|
|
50
|
+
"yarn.lock",
|
|
51
|
+
"pnpm-lock.yaml",
|
|
52
|
+
"composer.lock",
|
|
53
|
+
"Gemfile.lock",
|
|
54
|
+
"poetry.lock", # lock files
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_SKIP_PATTERNS = [
|
|
58
|
+
re.compile(r"\.min\.(js|css|mjs)$"), # minified
|
|
59
|
+
re.compile(r"\.snap$"), # Jest snapshots
|
|
60
|
+
re.compile(r"^(dist|build|\.next|out|\.nuxt)/"), # build artifacts
|
|
61
|
+
re.compile(r"^__generated__/"), # GraphQL / codegen
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Maximum diff characters sent per AI call for a single hunk-group
|
|
65
|
+
MAX_CHUNK_CHARS = 8_000
|
|
66
|
+
# Maximum added lines before we split a file's diff into multiple chunks
|
|
67
|
+
HUNK_GROUP_SIZE = 50
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _should_skip(filepath: str) -> Optional[str]:
|
|
71
|
+
"""Return a human-readable skip reason, or None if the file should be reviewed."""
|
|
72
|
+
import os
|
|
73
|
+
|
|
74
|
+
basename = os.path.basename(filepath)
|
|
75
|
+
ext = os.path.splitext(basename)[1].lower()
|
|
76
|
+
|
|
77
|
+
if ext in _SKIP_EXTENSIONS:
|
|
78
|
+
return f"binary/image ({ext})"
|
|
79
|
+
if basename in _SKIP_FILENAMES:
|
|
80
|
+
return "lock file"
|
|
81
|
+
for pat in _SKIP_PATTERNS:
|
|
82
|
+
if pat.search(filepath):
|
|
83
|
+
return f"matches skip pattern ({pat.pattern})"
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Core diff parser ──────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _parse_raw_files(diff_text: str) -> list[dict]:
|
|
91
|
+
"""
|
|
92
|
+
Split a unified diff into a list of per-file raw dicts:
|
|
93
|
+
{file, hunks: [{header, lines, added_count}]}
|
|
94
|
+
"""
|
|
95
|
+
files = []
|
|
96
|
+
current_file: Optional[str] = None
|
|
97
|
+
current_hunks: list = []
|
|
98
|
+
current_hunk: Optional[dict] = None
|
|
99
|
+
|
|
100
|
+
for line in diff_text.splitlines(keepends=True):
|
|
101
|
+
if line.startswith("diff --git"):
|
|
102
|
+
if current_file is not None:
|
|
103
|
+
if current_hunk:
|
|
104
|
+
current_hunks.append(current_hunk)
|
|
105
|
+
files.append({"file": current_file, "hunks": current_hunks})
|
|
106
|
+
current_file = None
|
|
107
|
+
current_hunks = []
|
|
108
|
+
current_hunk = None
|
|
109
|
+
elif line.startswith("+++ b/"):
|
|
110
|
+
current_file = line[6:].rstrip("\n")
|
|
111
|
+
elif line.startswith("@@ "):
|
|
112
|
+
if current_hunk:
|
|
113
|
+
current_hunks.append(current_hunk)
|
|
114
|
+
current_hunk = {"header": line, "lines": [], "added_count": 0}
|
|
115
|
+
elif current_hunk is not None:
|
|
116
|
+
current_hunk["lines"].append(line)
|
|
117
|
+
if line.startswith("+") and not line.startswith("+++"):
|
|
118
|
+
current_hunk["added_count"] += 1
|
|
119
|
+
|
|
120
|
+
# flush last file
|
|
121
|
+
if current_file is not None:
|
|
122
|
+
if current_hunk:
|
|
123
|
+
current_hunks.append(current_hunk)
|
|
124
|
+
files.append({"file": current_file, "hunks": current_hunks})
|
|
125
|
+
|
|
126
|
+
return files
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _hunk_to_text(hunk: dict) -> str:
|
|
130
|
+
return hunk["header"] + "".join(hunk["lines"])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_diff_into_file_chunks(diff_text: str) -> list[dict]:
|
|
134
|
+
"""
|
|
135
|
+
Main entry point for the piecemeal review pipeline.
|
|
136
|
+
|
|
137
|
+
Returns a list of chunk dicts:
|
|
138
|
+
{
|
|
139
|
+
"file": str, # relative path
|
|
140
|
+
"chunk_index": int, # 0-based (most files have only one chunk)
|
|
141
|
+
"total_chunks": int,
|
|
142
|
+
"skip": bool,
|
|
143
|
+
"reason": str | None, # populated when skip=True
|
|
144
|
+
"diff_text": str, # the raw diff text for this chunk
|
|
145
|
+
"added_lines": int,
|
|
146
|
+
"truncated": bool, # True when diff_text was trimmed to MAX_CHUNK_CHARS
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
raw_files = _parse_raw_files(diff_text)
|
|
150
|
+
chunks = []
|
|
151
|
+
|
|
152
|
+
for rf in raw_files:
|
|
153
|
+
filepath = rf["file"]
|
|
154
|
+
hunks = rf["hunks"]
|
|
155
|
+
|
|
156
|
+
skip_reason = _should_skip(filepath)
|
|
157
|
+
|
|
158
|
+
if skip_reason or not hunks:
|
|
159
|
+
chunks.append(
|
|
160
|
+
{
|
|
161
|
+
"file": filepath,
|
|
162
|
+
"chunk_index": 0,
|
|
163
|
+
"total_chunks": 1,
|
|
164
|
+
"skip": True,
|
|
165
|
+
"reason": skip_reason or "no hunks",
|
|
166
|
+
"diff_text": "",
|
|
167
|
+
"added_lines": 0,
|
|
168
|
+
"truncated": False,
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Group hunks into batches of ≤ HUNK_GROUP_SIZE added lines each
|
|
174
|
+
groups: list[list[dict]] = []
|
|
175
|
+
current_group: list[dict] = []
|
|
176
|
+
current_added = 0
|
|
177
|
+
|
|
178
|
+
for hunk in hunks:
|
|
179
|
+
if current_added + hunk["added_count"] > HUNK_GROUP_SIZE and current_group:
|
|
180
|
+
groups.append(current_group)
|
|
181
|
+
current_group = []
|
|
182
|
+
current_added = 0
|
|
183
|
+
current_group.append(hunk)
|
|
184
|
+
current_added += hunk["added_count"]
|
|
185
|
+
|
|
186
|
+
if current_group:
|
|
187
|
+
groups.append(current_group)
|
|
188
|
+
|
|
189
|
+
total_chunks = len(groups)
|
|
190
|
+
|
|
191
|
+
for idx, group in enumerate(groups):
|
|
192
|
+
text = f"--- a/{filepath}\n+++ b/{filepath}\n"
|
|
193
|
+
text += "".join(_hunk_to_text(h) for h in group)
|
|
194
|
+
added = sum(h["added_count"] for h in group)
|
|
195
|
+
truncated = False
|
|
196
|
+
|
|
197
|
+
if len(text) > MAX_CHUNK_CHARS:
|
|
198
|
+
text = text[:MAX_CHUNK_CHARS] + f"\n... (diff truncated at {MAX_CHUNK_CHARS} chars)"
|
|
199
|
+
truncated = True
|
|
200
|
+
|
|
201
|
+
chunks.append(
|
|
202
|
+
{
|
|
203
|
+
"file": filepath,
|
|
204
|
+
"chunk_index": idx,
|
|
205
|
+
"total_chunks": total_chunks,
|
|
206
|
+
"skip": False,
|
|
207
|
+
"reason": None,
|
|
208
|
+
"diff_text": text,
|
|
209
|
+
"added_lines": added,
|
|
210
|
+
"truncated": truncated,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return chunks
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
raw = sys.stdin.read()
|
|
219
|
+
chunks = parse_diff_into_file_chunks(raw)
|
|
220
|
+
reviewable = [c for c in chunks if not c["skip"]]
|
|
221
|
+
skipped = [c for c in chunks if c["skip"]]
|
|
222
|
+
print(
|
|
223
|
+
json.dumps(
|
|
224
|
+
{
|
|
225
|
+
"total_chunks": len(chunks),
|
|
226
|
+
"reviewable": len(reviewable),
|
|
227
|
+
"skipped": len(skipped),
|
|
228
|
+
"chunks": chunks,
|
|
229
|
+
},
|
|
230
|
+
indent=2,
|
|
231
|
+
)
|
|
232
|
+
)
|
dev_tools/schema_gen.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# pylint: disable=consider-using-from-import,import-outside-toplevel,missing-docstring
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import dev_tools.models as models
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_models_schema():
|
|
11
|
+
schemas = {}
|
|
12
|
+
for name, obj in inspect.getmembers(models):
|
|
13
|
+
if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj is not BaseModel:
|
|
14
|
+
# Skip base classes if any
|
|
15
|
+
if name in ("CLIResponse", "CLIInput"):
|
|
16
|
+
continue
|
|
17
|
+
# Force export using field names (camelCase) instead of aliases (snake_case)
|
|
18
|
+
schemas[name] = obj.model_json_schema(by_alias=False)
|
|
19
|
+
return schemas
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_schema():
|
|
23
|
+
schemas = {
|
|
24
|
+
"_warning": "AUTO-GENERATED: DO NOT EDIT MANUALLY. Update models.py instead.",
|
|
25
|
+
"models": get_models_schema(),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Use cli-schema.json as the unified target
|
|
29
|
+
output_path = os.path.join(os.path.dirname(__file__), "cli-schema.json")
|
|
30
|
+
|
|
31
|
+
# Read existing if exists to merge
|
|
32
|
+
existing = {}
|
|
33
|
+
if os.path.exists(output_path):
|
|
34
|
+
try:
|
|
35
|
+
with open(output_path, "r", encoding="utf-8") as f:
|
|
36
|
+
existing = json.load(f)
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f"Warning: Could not read existing cli-schema.json: {e}")
|
|
39
|
+
|
|
40
|
+
existing.update(schemas)
|
|
41
|
+
|
|
42
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
43
|
+
json.dump(existing, f, indent=2)
|
|
44
|
+
f.write("\n")
|
|
45
|
+
|
|
46
|
+
import sys
|
|
47
|
+
|
|
48
|
+
print(
|
|
49
|
+
f"Updated cli-schema.json with {len(schemas['models'])} models at {output_path}",
|
|
50
|
+
file=sys.stderr,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
generate_schema()
|