reposeed 0.4.0__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.
- reposeed/__init__.py +10 -0
- reposeed/__main__.py +3 -0
- reposeed/cli.py +232 -0
- reposeed/constants.py +35 -0
- reposeed/detect.py +89 -0
- reposeed/doctor.py +122 -0
- reposeed/errors.py +4 -0
- reposeed/foundation/AGENTS.md +108 -0
- reposeed/foundation/CHANGELOG.md +5 -0
- reposeed/foundation/GRAPHIFY.md +96 -0
- reposeed/foundation/OPERATING_GUIDELINES.md +102 -0
- reposeed/foundation/PLATFORM_SUPPORT.md +69 -0
- reposeed/foundation/skills/flutter/flutter-conventions/SKILL.md +39 -0
- reposeed/foundation/skills/flutter/flutter-conventions/agents/openai.yaml +4 -0
- reposeed/foundation/skills/node/node-conventions/SKILL.md +37 -0
- reposeed/foundation/skills/node/node-conventions/agents/openai.yaml +4 -0
- reposeed/foundation/skills/php/php-conventions/SKILL.md +35 -0
- reposeed/foundation/skills/php/php-conventions/agents/openai.yaml +4 -0
- reposeed/foundation/skills/python/python-conventions/SKILL.md +39 -0
- reposeed/foundation/skills/python/python-conventions/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/clarify-before-building/SKILL.md +61 -0
- reposeed/foundation/skills/universal/clarify-before-building/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/code-review/SKILL.md +86 -0
- reposeed/foundation/skills/universal/code-review/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/commit-messages/SKILL.md +67 -0
- reposeed/foundation/skills/universal/commit-messages/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/compounding-knowledge/SKILL.md +59 -0
- reposeed/foundation/skills/universal/compounding-knowledge/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/diagnosing-bugs/SKILL.md +89 -0
- reposeed/foundation/skills/universal/diagnosing-bugs/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/maintain-changelog/SKILL.md +60 -0
- reposeed/foundation/skills/universal/maintain-changelog/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/pr-description/SKILL.md +55 -0
- reposeed/foundation/skills/universal/pr-description/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/security-checklist/SKILL.md +66 -0
- reposeed/foundation/skills/universal/security-checklist/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/tdd/SKILL.md +79 -0
- reposeed/foundation/skills/universal/tdd/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/verification-before-completion/SKILL.md +74 -0
- reposeed/foundation/skills/universal/verification-before-completion/agents/openai.yaml +4 -0
- reposeed/foundation/skills/universal/writing-plans/SKILL.md +89 -0
- reposeed/foundation/skills/universal/writing-plans/agents/openai.yaml +4 -0
- reposeed/graphify.py +556 -0
- reposeed/initializer.py +328 -0
- reposeed/managed_file.py +147 -0
- reposeed/templates.py +66 -0
- reposeed/validation.py +26 -0
- reposeed-0.4.0.dist-info/METADATA +248 -0
- reposeed-0.4.0.dist-info/RECORD +52 -0
- reposeed-0.4.0.dist-info/WHEEL +4 -0
- reposeed-0.4.0.dist-info/entry_points.txt +3 -0
- reposeed-0.4.0.dist-info/licenses/LICENSE +21 -0
reposeed/__init__.py
ADDED
reposeed/__main__.py
ADDED
reposeed/cli.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from reposeed import __version__
|
|
10
|
+
from reposeed.constants import PLATFORM_LABELS, PLATFORMS, STACK_LABELS, STACKS
|
|
11
|
+
from reposeed.detect import detect_project
|
|
12
|
+
from reposeed.doctor import diagnose_project
|
|
13
|
+
from reposeed.errors import CliError
|
|
14
|
+
from reposeed.graphify import graphify_python, run_graphify
|
|
15
|
+
from reposeed.initializer import initialize_project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
19
|
+
arguments = list(argv if argv is not None else sys.argv[1:])
|
|
20
|
+
try:
|
|
21
|
+
if arguments[:1] == ["graphify"]:
|
|
22
|
+
return run_graphify(arguments[1:])
|
|
23
|
+
if arguments[:1] == ["_graphify-python"]:
|
|
24
|
+
return graphify_python()
|
|
25
|
+
|
|
26
|
+
parser = _create_parser()
|
|
27
|
+
namespace = parser.parse_args(arguments)
|
|
28
|
+
if namespace.command == "init":
|
|
29
|
+
return _run_init(namespace)
|
|
30
|
+
if namespace.command == "doctor":
|
|
31
|
+
return _run_doctor(namespace)
|
|
32
|
+
parser.print_help()
|
|
33
|
+
return 0
|
|
34
|
+
except CliError as error:
|
|
35
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
36
|
+
return error.exit_code
|
|
37
|
+
except KeyboardInterrupt:
|
|
38
|
+
print("\nError: Initialization cancelled.", file=sys.stderr)
|
|
39
|
+
return 130
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _create_parser() -> argparse.ArgumentParser:
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
prog="reposeed",
|
|
45
|
+
description="Initialize portable agent instructions and skills for AI coding tools.",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
48
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
49
|
+
|
|
50
|
+
init = subparsers.add_parser("init", help="initialize agent standards in a project")
|
|
51
|
+
init.add_argument("directory", nargs="?", default=".", help="project directory")
|
|
52
|
+
init.add_argument(
|
|
53
|
+
"-p",
|
|
54
|
+
"--platforms",
|
|
55
|
+
help="comma-separated: codex, claude, cursor, antigravity",
|
|
56
|
+
)
|
|
57
|
+
init.add_argument(
|
|
58
|
+
"-s",
|
|
59
|
+
"--stacks",
|
|
60
|
+
help="comma-separated: node, python, php, flutter (default: auto-detect)",
|
|
61
|
+
)
|
|
62
|
+
init.add_argument("-y", "--yes", action="store_true", help="accept defaults")
|
|
63
|
+
init.add_argument(
|
|
64
|
+
"--no-graphify",
|
|
65
|
+
action="store_false",
|
|
66
|
+
dest="graphify",
|
|
67
|
+
default=True,
|
|
68
|
+
help="skip managed Graphify integration",
|
|
69
|
+
)
|
|
70
|
+
graphify_git_hooks = init.add_mutually_exclusive_group()
|
|
71
|
+
graphify_git_hooks.add_argument(
|
|
72
|
+
"--graphify-git-hooks",
|
|
73
|
+
action="store_true",
|
|
74
|
+
dest="graphify_git_hooks",
|
|
75
|
+
help="install Graphify Git auto-rebuild hooks (default with Graphify)",
|
|
76
|
+
)
|
|
77
|
+
graphify_git_hooks.add_argument(
|
|
78
|
+
"--no-graphify-git-hooks",
|
|
79
|
+
action="store_false",
|
|
80
|
+
dest="graphify_git_hooks",
|
|
81
|
+
help="skip or remove the default Graphify Git hooks",
|
|
82
|
+
)
|
|
83
|
+
init.set_defaults(graphify_git_hooks=None)
|
|
84
|
+
init.add_argument("--dry-run", action="store_true", help="show changes without writing")
|
|
85
|
+
init.add_argument("--force", action="store_true", help="replace conflicting generated files")
|
|
86
|
+
init.add_argument("--json", action="store_true", dest="json_output")
|
|
87
|
+
|
|
88
|
+
doctor = subparsers.add_parser("doctor", help="validate an initialized project")
|
|
89
|
+
doctor.add_argument("directory", nargs="?", default=".", help="project directory")
|
|
90
|
+
doctor.add_argument("--json", action="store_true", dest="json_output")
|
|
91
|
+
|
|
92
|
+
graphify = subparsers.add_parser(
|
|
93
|
+
"graphify",
|
|
94
|
+
help="run Graphify from the managed uvx environment",
|
|
95
|
+
)
|
|
96
|
+
graphify.add_argument("arguments", nargs=argparse.REMAINDER)
|
|
97
|
+
return parser
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _run_init(namespace: argparse.Namespace) -> int:
|
|
101
|
+
target = Path(namespace.directory).resolve()
|
|
102
|
+
detected = detect_project(target)
|
|
103
|
+
selected_platforms = _parse_list(namespace.platforms)
|
|
104
|
+
selected_stacks = _parse_list(namespace.stacks) if namespace.stacks is not None else None
|
|
105
|
+
|
|
106
|
+
if not namespace.yes and sys.stdin.isatty() and namespace.platforms is None:
|
|
107
|
+
selected_platforms = _prompt_selection(
|
|
108
|
+
"Coding agents",
|
|
109
|
+
list(PLATFORMS),
|
|
110
|
+
PLATFORM_LABELS,
|
|
111
|
+
list(PLATFORMS),
|
|
112
|
+
required=True,
|
|
113
|
+
)
|
|
114
|
+
if namespace.stacks is None:
|
|
115
|
+
selected_stacks = _prompt_selection(
|
|
116
|
+
"Application stacks",
|
|
117
|
+
list(STACKS),
|
|
118
|
+
STACK_LABELS,
|
|
119
|
+
list(detected["stacks"]),
|
|
120
|
+
required=False,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
selected_platforms = selected_platforms or list(PLATFORMS)
|
|
124
|
+
if selected_stacks is None:
|
|
125
|
+
selected_stacks = list(detected["stacks"])
|
|
126
|
+
|
|
127
|
+
def report(message: str) -> None:
|
|
128
|
+
if not namespace.json_output:
|
|
129
|
+
print(f"→ {message}")
|
|
130
|
+
|
|
131
|
+
result = initialize_project(
|
|
132
|
+
target_directory=target,
|
|
133
|
+
platforms=selected_platforms,
|
|
134
|
+
stacks=selected_stacks,
|
|
135
|
+
graphify=namespace.graphify,
|
|
136
|
+
graphify_git_hooks=namespace.graphify_git_hooks,
|
|
137
|
+
dry_run=namespace.dry_run,
|
|
138
|
+
force=namespace.force,
|
|
139
|
+
report=report,
|
|
140
|
+
)
|
|
141
|
+
if namespace.json_output:
|
|
142
|
+
print(json.dumps(result, indent=2))
|
|
143
|
+
else:
|
|
144
|
+
print(_format_init_result(result, namespace.dry_run))
|
|
145
|
+
return 2 if any(item["status"] == "conflict" for item in result["operations"]) else 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _run_doctor(namespace: argparse.Namespace) -> int:
|
|
149
|
+
result = diagnose_project(Path(namespace.directory))
|
|
150
|
+
if namespace.json_output:
|
|
151
|
+
print(json.dumps(result, indent=2))
|
|
152
|
+
elif result["ok"]:
|
|
153
|
+
print(
|
|
154
|
+
f"Healthy: {len(result['checks'])} managed files verified in "
|
|
155
|
+
f"{result['targetDirectory']}"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
print(f"Found {len(result['issues'])} issue(s) in {result['targetDirectory']}:")
|
|
159
|
+
for issue in result["issues"]:
|
|
160
|
+
print(f"- {issue}")
|
|
161
|
+
return 0 if result["ok"] else 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _parse_list(value: str | None) -> list[str] | None:
|
|
165
|
+
if value is None:
|
|
166
|
+
return None
|
|
167
|
+
return list(dict.fromkeys(item.strip() for item in value.split(",") if item.strip()))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _prompt_selection(
|
|
171
|
+
title: str,
|
|
172
|
+
choices: list[str],
|
|
173
|
+
labels: dict[str, str],
|
|
174
|
+
defaults: list[str],
|
|
175
|
+
*,
|
|
176
|
+
required: bool,
|
|
177
|
+
) -> list[str]:
|
|
178
|
+
print(f"\n{title}:")
|
|
179
|
+
for index, choice in enumerate(choices, start=1):
|
|
180
|
+
suffix = " (default)" if choice in defaults else ""
|
|
181
|
+
print(f" {index}. {labels[choice]}{suffix}")
|
|
182
|
+
default_text = ",".join(defaults)
|
|
183
|
+
answer = input(f"Choose comma-separated names or numbers [{default_text}]: ").strip()
|
|
184
|
+
if not answer:
|
|
185
|
+
selected = defaults
|
|
186
|
+
else:
|
|
187
|
+
selected = []
|
|
188
|
+
for item in (part.strip() for part in answer.split(",")):
|
|
189
|
+
selected.append(choices[int(item) - 1] if item.isdigit() else item)
|
|
190
|
+
selected = list(dict.fromkeys(selected))
|
|
191
|
+
if required and not selected:
|
|
192
|
+
raise CliError("Select at least one option.")
|
|
193
|
+
return selected
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _format_init_result(result: dict[str, object], dry_run: bool) -> str:
|
|
197
|
+
operations = result["operations"]
|
|
198
|
+
groups: dict[str, list[dict[str, str]]] = {}
|
|
199
|
+
for operation in operations: # type: ignore[union-attr]
|
|
200
|
+
groups.setdefault(operation["status"], []).append(operation)
|
|
201
|
+
platforms = ", ".join(result["platforms"]) # type: ignore[arg-type]
|
|
202
|
+
stacks = ", ".join(result["stacks"]) or "universal only" # type: ignore[arg-type]
|
|
203
|
+
lines = [
|
|
204
|
+
f"{'Planned' if dry_run else 'Initialized'} {result['targetDirectory']}",
|
|
205
|
+
f"Platforms: {platforms}",
|
|
206
|
+
f"Stacks: {stacks}",
|
|
207
|
+
"Files: "
|
|
208
|
+
f"{len(groups.get('created', []))} created, "
|
|
209
|
+
f"{len(groups.get('updated', []))} updated, "
|
|
210
|
+
f"{len(groups.get('unchanged', []))} unchanged",
|
|
211
|
+
]
|
|
212
|
+
if groups.get("conflict"):
|
|
213
|
+
paths = ", ".join(item["path"] for item in groups["conflict"])
|
|
214
|
+
lines.extend((f"Conflicts preserved: {paths}", "Review those files or rerun with --force."))
|
|
215
|
+
graphify = result["graphify"]
|
|
216
|
+
if graphify["enabled"]: # type: ignore[index]
|
|
217
|
+
status = "planned" if dry_run else "verified and configured"
|
|
218
|
+
lines.append(
|
|
219
|
+
f"Graphify {graphify['version']}: {status} through the uvx environment" # type: ignore[index]
|
|
220
|
+
)
|
|
221
|
+
for item in graphify["platforms"]: # type: ignore[index]
|
|
222
|
+
if item.get("platform"):
|
|
223
|
+
lines.append(f"Graphify ({item['platform']}): {item['status']}")
|
|
224
|
+
git_hooks = graphify.get("gitHooks", {}) # type: ignore[union-attr]
|
|
225
|
+
if git_hooks.get("enabled") or git_hooks.get("status") in {
|
|
226
|
+
"removed",
|
|
227
|
+
"planned-removal",
|
|
228
|
+
"skipped-no-git",
|
|
229
|
+
}:
|
|
230
|
+
lines.append(f"Graphify Git hooks: {git_hooks['status']}")
|
|
231
|
+
lines.append("Run `uvx reposeed doctor` to verify the installation.")
|
|
232
|
+
return "\n".join(lines)
|
reposeed/constants.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
FOUNDATION_ROOT = files("reposeed").joinpath("foundation")
|
|
6
|
+
|
|
7
|
+
PLATFORMS = ("codex", "claude", "cursor", "antigravity")
|
|
8
|
+
STACKS = ("node", "python", "php", "flutter")
|
|
9
|
+
|
|
10
|
+
PLATFORM_LABELS = {
|
|
11
|
+
"codex": "Codex",
|
|
12
|
+
"claude": "Claude Code",
|
|
13
|
+
"cursor": "Cursor",
|
|
14
|
+
"antigravity": "Antigravity",
|
|
15
|
+
}
|
|
16
|
+
STACK_LABELS = {
|
|
17
|
+
"node": "Node.js / TypeScript",
|
|
18
|
+
"python": "Python",
|
|
19
|
+
"php": "PHP",
|
|
20
|
+
"flutter": "Flutter / Dart",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
MANAGED_BEGIN = "<!-- reposeed:begin -->"
|
|
24
|
+
MANAGED_END = "<!-- reposeed:end -->"
|
|
25
|
+
MANIFEST_PATH = ".reposeed/manifest.json"
|
|
26
|
+
CONFIG_PATH = ".reposeed/config.json"
|
|
27
|
+
|
|
28
|
+
GRAPHIFY_PACKAGE = "graphifyy"
|
|
29
|
+
GRAPHIFY_VERSION = "0.9.49"
|
|
30
|
+
GRAPHIFY_PLATFORMS = {
|
|
31
|
+
"codex": "codex",
|
|
32
|
+
"claude": "claude",
|
|
33
|
+
"cursor": "cursor",
|
|
34
|
+
"antigravity": "antigravity",
|
|
35
|
+
}
|
reposeed/detect.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _read_json(path: Path) -> dict[str, Any] | None:
|
|
9
|
+
try:
|
|
10
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
11
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
12
|
+
return None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_project(target_directory: Path) -> dict[str, list[Any]]:
|
|
16
|
+
package_json = _read_json(target_directory / "package.json")
|
|
17
|
+
stacks: list[str] = []
|
|
18
|
+
if package_json:
|
|
19
|
+
stacks.append("node")
|
|
20
|
+
if any(
|
|
21
|
+
(target_directory / name).exists()
|
|
22
|
+
for name in ("pyproject.toml", "requirements.txt", "setup.py")
|
|
23
|
+
):
|
|
24
|
+
stacks.append("python")
|
|
25
|
+
if any((target_directory / name).exists() for name in ("composer.json", "artisan")):
|
|
26
|
+
stacks.append("php")
|
|
27
|
+
if (target_directory / "pubspec.yaml").exists():
|
|
28
|
+
stacks.append("flutter")
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"stacks": list(dict.fromkeys(stacks)),
|
|
32
|
+
"commands": _detect_commands(target_directory, package_json),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _detect_commands(
|
|
37
|
+
target_directory: Path, package_json: dict[str, Any] | None
|
|
38
|
+
) -> list[dict[str, str]]:
|
|
39
|
+
commands: list[dict[str, str]] = []
|
|
40
|
+
if package_json:
|
|
41
|
+
runner, install = "npm", "npm install"
|
|
42
|
+
if (target_directory / "pnpm-lock.yaml").exists():
|
|
43
|
+
runner, install = "pnpm", "pnpm install"
|
|
44
|
+
elif (target_directory / "yarn.lock").exists():
|
|
45
|
+
runner, install = "yarn", "yarn install"
|
|
46
|
+
elif any((target_directory / name).exists() for name in ("bun.lock", "bun.lockb")):
|
|
47
|
+
runner, install = "bun", "bun install"
|
|
48
|
+
commands.append({"label": "Install", "command": install})
|
|
49
|
+
scripts = package_json.get("scripts", {})
|
|
50
|
+
for name in ("lint", "typecheck", "test", "build"):
|
|
51
|
+
if scripts.get(name):
|
|
52
|
+
separator = " run " if runner == "npm" else " "
|
|
53
|
+
commands.append({"label": name.title(), "command": f"{runner}{separator}{name}"})
|
|
54
|
+
|
|
55
|
+
if (target_directory / "pyproject.toml").exists():
|
|
56
|
+
if (target_directory / "uv.lock").exists():
|
|
57
|
+
commands.append({"label": "Install Python", "command": "uv sync"})
|
|
58
|
+
commands.append({"label": "Test Python", "command": "pytest"})
|
|
59
|
+
elif (target_directory / "requirements.txt").exists():
|
|
60
|
+
commands.extend(
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
"label": "Install Python",
|
|
64
|
+
"command": "python -m pip install -r requirements.txt",
|
|
65
|
+
},
|
|
66
|
+
{"label": "Test Python", "command": "pytest"},
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if (target_directory / "composer.json").exists():
|
|
71
|
+
commands.extend(
|
|
72
|
+
[
|
|
73
|
+
{"label": "Install PHP", "command": "composer install"},
|
|
74
|
+
{"label": "Test PHP", "command": "composer test"},
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
if (target_directory / "pubspec.yaml").exists():
|
|
78
|
+
commands.extend(
|
|
79
|
+
[
|
|
80
|
+
{"label": "Install Flutter", "command": "flutter pub get"},
|
|
81
|
+
{"label": "Analyze Flutter", "command": "flutter analyze"},
|
|
82
|
+
{"label": "Test Flutter", "command": "flutter test"},
|
|
83
|
+
]
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
unique: dict[str, dict[str, str]] = {}
|
|
87
|
+
for command in commands:
|
|
88
|
+
unique.setdefault(command["command"], command)
|
|
89
|
+
return list(unique.values())
|
reposeed/doctor.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from reposeed.constants import CONFIG_PATH, GRAPHIFY_VERSION, MANIFEST_PATH
|
|
6
|
+
from reposeed.graphify import (
|
|
7
|
+
RunCommand,
|
|
8
|
+
default_run_command,
|
|
9
|
+
expected_graphify_files,
|
|
10
|
+
graphify_git_hooks_healthy,
|
|
11
|
+
inspect_graphify,
|
|
12
|
+
inspect_graphify_git_hooks,
|
|
13
|
+
)
|
|
14
|
+
from reposeed.managed_file import content_hash, extract_managed_block, read_json, read_text
|
|
15
|
+
from reposeed.validation import validate_skill
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def diagnose_project(
|
|
19
|
+
target_directory: Path, run_command: RunCommand = default_run_command
|
|
20
|
+
) -> dict[str, object]:
|
|
21
|
+
root = target_directory.resolve()
|
|
22
|
+
config = read_json(root / CONFIG_PATH)
|
|
23
|
+
manifest = read_json(root / MANIFEST_PATH)
|
|
24
|
+
issues: list[str] = []
|
|
25
|
+
checks: list[str] = []
|
|
26
|
+
|
|
27
|
+
if not config:
|
|
28
|
+
issues.append(f"{CONFIG_PATH} is missing; run `uvx reposeed init`.")
|
|
29
|
+
if not manifest:
|
|
30
|
+
issues.append(f"{MANIFEST_PATH} is missing; run `uvx reposeed init`.")
|
|
31
|
+
|
|
32
|
+
if manifest:
|
|
33
|
+
for relative_path, record in manifest.get("files", {}).items():
|
|
34
|
+
content = read_text(root / relative_path)
|
|
35
|
+
if content is None:
|
|
36
|
+
issues.append(f"{relative_path} is missing.")
|
|
37
|
+
continue
|
|
38
|
+
tracked = (
|
|
39
|
+
extract_managed_block(content) if record.get("mode") == "managed-block" else content
|
|
40
|
+
)
|
|
41
|
+
if tracked is None or content_hash(tracked) != record.get("sha256"):
|
|
42
|
+
issues.append(f"{relative_path} has local changes.")
|
|
43
|
+
else:
|
|
44
|
+
checks.append(f"{relative_path} matches the manifest.")
|
|
45
|
+
|
|
46
|
+
skills_directory = root / ".agents" / "skills"
|
|
47
|
+
if not skills_directory.is_dir():
|
|
48
|
+
issues.append(".agents/skills is missing.")
|
|
49
|
+
else:
|
|
50
|
+
for entry in sorted(skills_directory.iterdir()):
|
|
51
|
+
if not entry.is_dir():
|
|
52
|
+
continue
|
|
53
|
+
relative_path = f".agents/skills/{entry.name}/SKILL.md"
|
|
54
|
+
content = read_text(root / relative_path)
|
|
55
|
+
if content is None:
|
|
56
|
+
issues.append(f"{relative_path} is missing.")
|
|
57
|
+
else:
|
|
58
|
+
issues.extend(validate_skill(content, entry.name, relative_path))
|
|
59
|
+
|
|
60
|
+
graphify_config = config.get("graphify") if config else None
|
|
61
|
+
graphify_enabled = graphify_config is True or (
|
|
62
|
+
isinstance(graphify_config, dict) and graphify_config.get("enabled") is True
|
|
63
|
+
)
|
|
64
|
+
if graphify_enabled:
|
|
65
|
+
extraction_mode = (
|
|
66
|
+
graphify_config.get("extractionMode") if isinstance(graphify_config, dict) else None
|
|
67
|
+
)
|
|
68
|
+
if extraction_mode != "ast-only":
|
|
69
|
+
issues.append(
|
|
70
|
+
"Managed Graphify is not configured for AST-only extraction; rerun "
|
|
71
|
+
"uvx reposeed init."
|
|
72
|
+
)
|
|
73
|
+
else:
|
|
74
|
+
checks.append("Managed Graphify extraction mode is AST-only.")
|
|
75
|
+
|
|
76
|
+
expected_version = (
|
|
77
|
+
graphify_config.get("version", GRAPHIFY_VERSION)
|
|
78
|
+
if isinstance(graphify_config, dict)
|
|
79
|
+
else GRAPHIFY_VERSION
|
|
80
|
+
)
|
|
81
|
+
installed = inspect_graphify(root, run_command)
|
|
82
|
+
if not installed:
|
|
83
|
+
issues.append("Managed Graphify runtime is unavailable in the uvx environment.")
|
|
84
|
+
elif installed["version"] != expected_version:
|
|
85
|
+
issues.append(
|
|
86
|
+
f"Managed Graphify version is {installed['version']}; expected {expected_version}."
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
checks.append(f"Managed Graphify {installed['version']} is available.")
|
|
90
|
+
|
|
91
|
+
graphify_platforms = (
|
|
92
|
+
graphify_config.get("platforms", config.get("platforms", []))
|
|
93
|
+
if isinstance(graphify_config, dict)
|
|
94
|
+
else config.get("platforms", [])
|
|
95
|
+
)
|
|
96
|
+
for relative_path in expected_graphify_files(graphify_platforms):
|
|
97
|
+
if read_text(root / relative_path) is None:
|
|
98
|
+
issues.append(f"{relative_path} is missing from the Graphify integration.")
|
|
99
|
+
else:
|
|
100
|
+
checks.append(f"{relative_path} is configured for Graphify.")
|
|
101
|
+
|
|
102
|
+
git_hooks = graphify_config.get("gitHooks", {}) if isinstance(graphify_config, dict) else {}
|
|
103
|
+
git_hooks_enabled = git_hooks is True or (
|
|
104
|
+
isinstance(git_hooks, dict) and git_hooks.get("enabled") is True
|
|
105
|
+
)
|
|
106
|
+
if git_hooks_enabled:
|
|
107
|
+
git_hook_status = inspect_graphify_git_hooks(root, run_command)
|
|
108
|
+
if graphify_git_hooks_healthy(git_hook_status):
|
|
109
|
+
checks.append("Graphify Git hooks are installed and current.")
|
|
110
|
+
else:
|
|
111
|
+
issues.append(
|
|
112
|
+
"Graphify Git hooks are missing or out of date; rerun "
|
|
113
|
+
"uvx reposeed init --graphify-git-hooks."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"ok": not issues,
|
|
118
|
+
"targetDirectory": str(root),
|
|
119
|
+
"config": config,
|
|
120
|
+
"checks": checks,
|
|
121
|
+
"issues": issues,
|
|
122
|
+
}
|
reposeed/errors.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Shared agent conventions
|
|
2
|
+
|
|
3
|
+
Use these conventions as the repository-wide baseline. Read
|
|
4
|
+
`AGENTS.local.md` before non-trivial work for this project's real commands and
|
|
5
|
+
exceptions. Guidance closer to the code takes precedence when it is more
|
|
6
|
+
specific.
|
|
7
|
+
|
|
8
|
+
## Before changing code
|
|
9
|
+
|
|
10
|
+
- Surface material assumptions. Inspect the repository first; ask only when a
|
|
11
|
+
choice cannot be discovered and different answers would materially change
|
|
12
|
+
the result. State low-risk, reversible assumptions while proceeding.
|
|
13
|
+
- Define a checkable completion criterion before implementation. Include the
|
|
14
|
+
behavior to preserve, the behavior to add, and the verification that will
|
|
15
|
+
prove both.
|
|
16
|
+
- Build the smallest coherent change that meets that criterion. Avoid
|
|
17
|
+
speculative abstractions and unrelated cleanup.
|
|
18
|
+
- Match the surrounding architecture and style. Record a nearby issue instead
|
|
19
|
+
of silently expanding the task to fix it.
|
|
20
|
+
- Use the `clarify-before-building` skill for ambiguous or non-trivial work.
|
|
21
|
+
- Use `writing-plans` when asked for an execution-ready implementation plan;
|
|
22
|
+
planning-only requests do not authorize code changes.
|
|
23
|
+
|
|
24
|
+
## Project knowledge
|
|
25
|
+
|
|
26
|
+
- Read `CONTEXT.md` before making a domain-model decision when it exists.
|
|
27
|
+
- Read relevant records in `docs/adr/` before changing an established
|
|
28
|
+
architectural seam. Add an ADR when a durable, consequential decision is
|
|
29
|
+
made and the repository uses that convention.
|
|
30
|
+
- Treat configuration, package scripts, tests, and existing code as the source
|
|
31
|
+
of truth. Keep `AGENTS.local.md` focused on conventions that cannot be
|
|
32
|
+
discovered cheaply from those files.
|
|
33
|
+
|
|
34
|
+
## Context efficiency
|
|
35
|
+
|
|
36
|
+
- Start with the smallest reliable source: targeted search, configuration, and
|
|
37
|
+
relevant code or test slices. Expand only when the evidence is insufficient.
|
|
38
|
+
- Reuse existing indexes, summaries, and cached results. Reference large
|
|
39
|
+
artifacts by path instead of pasting or rereading them in full.
|
|
40
|
+
- Keep plans, progress updates, and handoffs concise and decision-focused.
|
|
41
|
+
- Use token-heavy semantic analysis or parallel agents only when their expected
|
|
42
|
+
value justifies the duplicated context; do not use them as routine discovery.
|
|
43
|
+
|
|
44
|
+
## Implementation baseline
|
|
45
|
+
|
|
46
|
+
- Use `diagnosing-bugs` for bugs, failures, regressions, or unexpected
|
|
47
|
+
behavior. Confirm root cause before proposing a fix, and keep diagnosis
|
|
48
|
+
read-only unless a fix was requested.
|
|
49
|
+
- Use `tdd` when the user explicitly requests test-first development or a
|
|
50
|
+
red-green-refactor workflow.
|
|
51
|
+
- Add or update tests for behavior changes. A bug fix needs a regression test
|
|
52
|
+
that fails without the fix. If a test is impractical, explain the concrete
|
|
53
|
+
reason and provide proportionate verification.
|
|
54
|
+
- Validate untrusted input at system boundaries. Keep authorization checks on
|
|
55
|
+
the resource being accessed, set timeouts on external calls, and avoid
|
|
56
|
+
leaking internal errors to users.
|
|
57
|
+
- Keep secrets out of source, fixtures, logs, prompts, and commit history. Use
|
|
58
|
+
the project's established secret and configuration mechanism.
|
|
59
|
+
- Prefer existing dependencies and standard-library capabilities. Check
|
|
60
|
+
maintenance, licensing, security, bundle/runtime cost, and project fit before
|
|
61
|
+
adding a production dependency.
|
|
62
|
+
- Preserve public interfaces unless the requested change explicitly includes a
|
|
63
|
+
migration. Document intentional breaking changes.
|
|
64
|
+
- Write comments for constraints and non-obvious reasoning, not a narration of
|
|
65
|
+
the code.
|
|
66
|
+
|
|
67
|
+
## Verification and delivery
|
|
68
|
+
|
|
69
|
+
- Run the narrowest relevant checks while iterating, then the project's full
|
|
70
|
+
lint, type-check, and test commands before handing off when practical.
|
|
71
|
+
- Report exactly what was verified and any remaining risk. Never describe a
|
|
72
|
+
check as passing if it was not run.
|
|
73
|
+
- Use `verification-before-completion` before claiming changed work is fixed,
|
|
74
|
+
passing, complete, or ready to ship.
|
|
75
|
+
- Use `code-review` for review-only requests and report actionable findings
|
|
76
|
+
before summaries. Do not modify reviewed code unless asked.
|
|
77
|
+
- Use `security-checklist` for auth, authorization, payments, user data, file
|
|
78
|
+
uploads, or external services.
|
|
79
|
+
- Use `maintain-changelog` before every commit so the dated changelog entry is
|
|
80
|
+
included in the same commit as the change.
|
|
81
|
+
- Use `commit-messages` when composing commits and `pr-description` when
|
|
82
|
+
preparing pull requests.
|
|
83
|
+
- Use `compounding-knowledge` after work that uncovered a reusable convention,
|
|
84
|
+
debugging technique, or repository-specific trap.
|
|
85
|
+
|
|
86
|
+
## Collaboration and source control
|
|
87
|
+
|
|
88
|
+
- Preserve user-authored and unrelated working-tree changes. Resolve overlap
|
|
89
|
+
explicitly instead of overwriting it.
|
|
90
|
+
- Keep commits and pull requests focused on one reviewable outcome. Follow the
|
|
91
|
+
repository's branch and review policy; never push or merge unless the user or
|
|
92
|
+
workflow authorizes it.
|
|
93
|
+
- Check for active parallel work before editing shared files when the project
|
|
94
|
+
provides an active-work registry or coordination mechanism.
|
|
95
|
+
|
|
96
|
+
## Optional knowledge graph
|
|
97
|
+
|
|
98
|
+
When `graphify-out/graph.json` exists, use `graphify query "<question>"` for
|
|
99
|
+
architectural discovery before broad repository scans, and set a small
|
|
100
|
+
`--budget` appropriate to the question. Prefer `query`, `path`, or `explain`
|
|
101
|
+
over loading the full report or raw graph. Preserve any Graphify-managed
|
|
102
|
+
sections when updating instruction files. See
|
|
103
|
+
`.reposeed/guides/GRAPHIFY.md` for setup and operating details.
|
|
104
|
+
After the final code edit in a task, refresh an existing graph with
|
|
105
|
+
`uvx reposeed graphify update .` before handoff. This AST-only refresh is
|
|
106
|
+
the complete managed update path. Managed Graphify is AST-only: do not run
|
|
107
|
+
semantic extraction, media transcription, deep mode, semantic deduplication,
|
|
108
|
+
or LLM community labeling through RepoSeed.
|