familiar-cli 0.0.5__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.
- familiar/__init__.py +7 -0
- familiar/agents.py +63 -0
- familiar/cli.py +278 -0
- familiar/data/invocations/__noop__.md +1 -0
- familiar/data/invocations/add-tests.md +68 -0
- familiar/data/invocations/bootstrap-python.md +102 -0
- familiar/data/invocations/bootstrap-rust.md +81 -0
- familiar/data/invocations/code-review.md +80 -0
- familiar/data/invocations/explain.md +42 -0
- familiar/data/invocations/implement-feature.md +75 -0
- familiar/data/invocations/infra-change.md +94 -0
- familiar/data/invocations/refactor.md +63 -0
- familiar/data/invocations/security-review.md +99 -0
- familiar/data/templates/core.md +46 -0
- familiar/data/templates/infra.md +68 -0
- familiar/data/templates/python.md +48 -0
- familiar/data/templates/rust.md +52 -0
- familiar/data/templates/sec.md +68 -0
- familiar/lint.py +229 -0
- familiar/render.py +112 -0
- familiar_cli-0.0.5.dist-info/METADATA +108 -0
- familiar_cli-0.0.5.dist-info/RECORD +26 -0
- familiar_cli-0.0.5.dist-info/WHEEL +5 -0
- familiar_cli-0.0.5.dist-info/entry_points.txt +2 -0
- familiar_cli-0.0.5.dist-info/licenses/LICENSE +21 -0
- familiar_cli-0.0.5.dist-info/top_level.txt +1 -0
familiar/__init__.py
ADDED
familiar/agents.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Agent implementations for familiar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Agent(ABC):
|
|
11
|
+
"""Base class for AI coding agents."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
output_file: str
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def run(self, repo_root: Path, prompt: str, headless: bool) -> int:
|
|
18
|
+
"""Run the agent with the given prompt."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CodexAgent(Agent):
|
|
22
|
+
name = "codex"
|
|
23
|
+
output_file = "AGENTS.md"
|
|
24
|
+
|
|
25
|
+
def run(self, repo_root: Path, prompt: str, headless: bool) -> int:
|
|
26
|
+
if headless:
|
|
27
|
+
cmd = ["codex", "exec", "-C", str(repo_root), "-"]
|
|
28
|
+
proc = subprocess.run(cmd, input=prompt, text=True)
|
|
29
|
+
return proc.returncode
|
|
30
|
+
else:
|
|
31
|
+
cmd = ["codex", "-C", str(repo_root), prompt]
|
|
32
|
+
return subprocess.call(cmd)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClaudeAgent(Agent):
|
|
36
|
+
name = "claude"
|
|
37
|
+
output_file = "CLAUDE.md"
|
|
38
|
+
|
|
39
|
+
def run(self, repo_root: Path, prompt: str, headless: bool) -> int:
|
|
40
|
+
# claude cli doesn't support a working directory flag like codex's -C;
|
|
41
|
+
# it uses cwd automatically, so repo_root is unused here
|
|
42
|
+
if headless:
|
|
43
|
+
cmd = ["claude", "-p", prompt]
|
|
44
|
+
else:
|
|
45
|
+
cmd = ["claude", prompt]
|
|
46
|
+
return subprocess.call(cmd)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
AGENTS: dict[str, Agent] = {
|
|
50
|
+
"codex": CodexAgent(),
|
|
51
|
+
"claude": ClaudeAgent(),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_agent(name: str) -> Agent:
|
|
56
|
+
"""Get an agent by name.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
KeyError: if the agent name is not recognized.
|
|
60
|
+
"""
|
|
61
|
+
if name not in AGENTS:
|
|
62
|
+
raise KeyError(f"unknown agent: {name}")
|
|
63
|
+
return AGENTS[name]
|
familiar/cli.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Command-line interface for familiar."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .agents import AGENTS, get_agent
|
|
12
|
+
from .lint import lint_all
|
|
13
|
+
from .render import render_invocation, compose_system, list_items, NotFoundError
|
|
14
|
+
|
|
15
|
+
# exit codes
|
|
16
|
+
EXIT_SUCCESS = 0
|
|
17
|
+
EXIT_ERROR = 1 # general error (agent failed, etc.)
|
|
18
|
+
EXIT_USAGE = 2 # usage error (bad args, missing files, etc.)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CliError(Exception):
|
|
22
|
+
"""CLI error with optional hint."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, message: str, hint: str | None = None, exit_code: int = EXIT_USAGE
|
|
26
|
+
):
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
self.hint = hint
|
|
29
|
+
self.exit_code = exit_code
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_repo_root(start: Path) -> Path:
|
|
33
|
+
"""Find the repository root by looking for .git directory.
|
|
34
|
+
|
|
35
|
+
Walks up from start directory. Falls back to start directory itself
|
|
36
|
+
if no .git is found, allowing use outside of git repositories.
|
|
37
|
+
"""
|
|
38
|
+
cur = start.resolve()
|
|
39
|
+
for p in [cur] + list(cur.parents):
|
|
40
|
+
if (p / ".git").exists():
|
|
41
|
+
return p
|
|
42
|
+
return cur
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def write_instruction(repo_root: Path, agent_name: str, system: str) -> None:
|
|
46
|
+
try:
|
|
47
|
+
agent = get_agent(agent_name)
|
|
48
|
+
except KeyError as e:
|
|
49
|
+
raise CliError(str(e), hint=f"valid agents: {', '.join(AGENTS.keys())}")
|
|
50
|
+
(repo_root / agent.output_file).write_text(system.strip() + "\n", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_kv(pairs: list[str]) -> dict[str, str]:
|
|
54
|
+
out: dict[str, str] = {}
|
|
55
|
+
for p in pairs:
|
|
56
|
+
if "=" not in p:
|
|
57
|
+
raise CliError(
|
|
58
|
+
f"invalid argument: {p}",
|
|
59
|
+
hint="use key=value format, e.g. --kv name=myproject",
|
|
60
|
+
)
|
|
61
|
+
k, v = p.split("=", 1)
|
|
62
|
+
out[k.strip()] = v.strip()
|
|
63
|
+
return out
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def run_agent(repo_root: Path, agent_name: str, prompt: str, headless: bool) -> int:
|
|
67
|
+
try:
|
|
68
|
+
agent = get_agent(agent_name)
|
|
69
|
+
except KeyError as e:
|
|
70
|
+
raise CliError(str(e), hint=f"valid agents: {', '.join(AGENTS.keys())}")
|
|
71
|
+
try:
|
|
72
|
+
return agent.run(repo_root, prompt, headless)
|
|
73
|
+
except FileNotFoundError:
|
|
74
|
+
raise CliError(
|
|
75
|
+
f"{agent_name} not found in PATH",
|
|
76
|
+
hint=f"install {agent_name} or check your PATH environment variable",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def cmd_conjure(args: argparse.Namespace) -> int:
|
|
81
|
+
repo_root = find_repo_root(Path(args.into or os.getcwd()))
|
|
82
|
+
try:
|
|
83
|
+
system = compose_system(repo_root, args.conjurings)
|
|
84
|
+
except NotFoundError as e:
|
|
85
|
+
raise CliError(
|
|
86
|
+
str(e),
|
|
87
|
+
hint="run 'familiar list conjurings' to see available options",
|
|
88
|
+
)
|
|
89
|
+
write_instruction(repo_root, args.agent, system)
|
|
90
|
+
print(f"wrote instructions for {args.agent}")
|
|
91
|
+
return EXIT_SUCCESS
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_invoke(args: argparse.Namespace) -> int:
|
|
95
|
+
repo_root = find_repo_root(Path(args.into or os.getcwd()))
|
|
96
|
+
kv = parse_kv(args.kv or [])
|
|
97
|
+
try:
|
|
98
|
+
prompt = render_invocation(repo_root, args.invocation, args.inv_args or [], kv)
|
|
99
|
+
except NotFoundError as e:
|
|
100
|
+
raise CliError(
|
|
101
|
+
str(e),
|
|
102
|
+
hint="run 'familiar list invocations' to see available options",
|
|
103
|
+
)
|
|
104
|
+
return run_agent(repo_root, args.agent, prompt, headless=args.headless)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _print_items(
|
|
108
|
+
items: list[tuple[str, str, bool]], verbose: bool, indent: str = ""
|
|
109
|
+
) -> None:
|
|
110
|
+
for name, first_line, is_local in items:
|
|
111
|
+
marker = " (local)" if is_local else ""
|
|
112
|
+
if verbose:
|
|
113
|
+
print(f"{indent}{name}{marker}: {first_line}")
|
|
114
|
+
else:
|
|
115
|
+
print(f"{indent}{name}{marker}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
119
|
+
repo_root = find_repo_root(Path(args.into or os.getcwd()))
|
|
120
|
+
|
|
121
|
+
if args.kind is None:
|
|
122
|
+
# list both
|
|
123
|
+
conjurings = list_items(repo_root, "templates")
|
|
124
|
+
invocations = list_items(repo_root, "invocations")
|
|
125
|
+
print("conjurings:")
|
|
126
|
+
_print_items(conjurings, args.verbose, indent=" ")
|
|
127
|
+
print("\ninvocations:")
|
|
128
|
+
_print_items(invocations, args.verbose, indent=" ")
|
|
129
|
+
return EXIT_SUCCESS
|
|
130
|
+
|
|
131
|
+
# map CLI names to internal names; "conjurings" is the user-facing term
|
|
132
|
+
# for what are stored internally as "templates"
|
|
133
|
+
kind = "templates" if args.kind == "conjurings" else args.kind
|
|
134
|
+
items = list_items(repo_root, kind)
|
|
135
|
+
|
|
136
|
+
if not items:
|
|
137
|
+
print(f"no {args.kind} found")
|
|
138
|
+
return EXIT_SUCCESS
|
|
139
|
+
|
|
140
|
+
_print_items(items, args.verbose)
|
|
141
|
+
return EXIT_SUCCESS
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def cmd_lint(args: argparse.Namespace) -> int:
|
|
145
|
+
repo_root = find_repo_root(Path(args.into or os.getcwd()))
|
|
146
|
+
|
|
147
|
+
messages = lint_all(repo_root)
|
|
148
|
+
|
|
149
|
+
# Filter by level if requested
|
|
150
|
+
if args.errors_only:
|
|
151
|
+
messages = [m for m in messages if m.level == "error"]
|
|
152
|
+
|
|
153
|
+
if not messages:
|
|
154
|
+
print("all checks passed")
|
|
155
|
+
return EXIT_SUCCESS
|
|
156
|
+
|
|
157
|
+
# Group by level for output
|
|
158
|
+
errors = [m for m in messages if m.level == "error"]
|
|
159
|
+
warnings = [m for m in messages if m.level == "warning"]
|
|
160
|
+
|
|
161
|
+
for msg in errors:
|
|
162
|
+
print(msg, file=sys.stderr)
|
|
163
|
+
for msg in warnings:
|
|
164
|
+
print(msg, file=sys.stderr)
|
|
165
|
+
|
|
166
|
+
if errors:
|
|
167
|
+
print(f"\n{len(errors)} error(s), {len(warnings)} warning(s)", file=sys.stderr)
|
|
168
|
+
return EXIT_ERROR
|
|
169
|
+
|
|
170
|
+
print(f"\n{len(warnings)} warning(s)", file=sys.stderr)
|
|
171
|
+
return EXIT_SUCCESS
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> None:
|
|
175
|
+
parser = argparse.ArgumentParser(
|
|
176
|
+
prog="familiar",
|
|
177
|
+
description="conjure and invoke familiars",
|
|
178
|
+
epilog="examples:\n"
|
|
179
|
+
" familiar conjure codex rust sec # create AGENTS.md\n"
|
|
180
|
+
" familiar invoke codex bootstrap-rust # run invocation\n"
|
|
181
|
+
" familiar list # show all options\n"
|
|
182
|
+
" familiar lint # validate prompts\n",
|
|
183
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
184
|
+
)
|
|
185
|
+
parser.add_argument(
|
|
186
|
+
"--debug", action="store_true", help="show full traceback on error"
|
|
187
|
+
)
|
|
188
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
189
|
+
|
|
190
|
+
agent_choices = list(AGENTS.keys())
|
|
191
|
+
|
|
192
|
+
conjure = sub.add_parser(
|
|
193
|
+
"conjure",
|
|
194
|
+
help="compose system instructions for an agent",
|
|
195
|
+
epilog="example: familiar conjure codex rust infra sec",
|
|
196
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
197
|
+
)
|
|
198
|
+
conjure.add_argument("agent", choices=agent_choices)
|
|
199
|
+
conjure.add_argument(
|
|
200
|
+
"conjurings", nargs="+", help="conjuring names, e.g. rust infra sec"
|
|
201
|
+
)
|
|
202
|
+
conjure.add_argument("--into", help="target repo path (default: current directory)")
|
|
203
|
+
conjure.set_defaults(func=cmd_conjure)
|
|
204
|
+
|
|
205
|
+
invoke = sub.add_parser(
|
|
206
|
+
"invoke",
|
|
207
|
+
help="render an invocation and run the agent",
|
|
208
|
+
epilog="example: familiar invoke codex bootstrap-rust myapp bin 1.78 mit",
|
|
209
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
210
|
+
)
|
|
211
|
+
invoke.add_argument("agent", choices=agent_choices)
|
|
212
|
+
invoke.add_argument("invocation")
|
|
213
|
+
invoke.add_argument("--into", help="target repo path (default: current directory)")
|
|
214
|
+
invoke.add_argument(
|
|
215
|
+
"--headless", action="store_true", help="run without interactive UI"
|
|
216
|
+
)
|
|
217
|
+
invoke.add_argument("--kv", nargs="*", help="named arguments as key=value pairs")
|
|
218
|
+
invoke.add_argument(
|
|
219
|
+
"inv_args", nargs="*", help="positional arguments for the invocation"
|
|
220
|
+
)
|
|
221
|
+
invoke.set_defaults(func=cmd_invoke)
|
|
222
|
+
|
|
223
|
+
list_cmd = sub.add_parser(
|
|
224
|
+
"list",
|
|
225
|
+
help="list available conjurings and invocations",
|
|
226
|
+
epilog="example: familiar list conjurings -v",
|
|
227
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
228
|
+
)
|
|
229
|
+
list_cmd.add_argument(
|
|
230
|
+
"kind",
|
|
231
|
+
nargs="?",
|
|
232
|
+
choices=["conjurings", "invocations"],
|
|
233
|
+
help="what to list (default: both)",
|
|
234
|
+
)
|
|
235
|
+
list_cmd.add_argument(
|
|
236
|
+
"--into", help="target repo path (default: current directory)"
|
|
237
|
+
)
|
|
238
|
+
list_cmd.add_argument(
|
|
239
|
+
"-v", "--verbose", action="store_true", help="show first line of each file"
|
|
240
|
+
)
|
|
241
|
+
list_cmd.set_defaults(func=cmd_list)
|
|
242
|
+
|
|
243
|
+
lint_cmd = sub.add_parser(
|
|
244
|
+
"lint",
|
|
245
|
+
help="lint templates and invocations",
|
|
246
|
+
epilog="example: familiar lint --errors-only",
|
|
247
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
248
|
+
)
|
|
249
|
+
lint_cmd.add_argument(
|
|
250
|
+
"--into", help="target repo path (default: current directory)"
|
|
251
|
+
)
|
|
252
|
+
lint_cmd.add_argument(
|
|
253
|
+
"--errors-only", action="store_true", help="show only errors, not warnings"
|
|
254
|
+
)
|
|
255
|
+
lint_cmd.set_defaults(func=cmd_lint)
|
|
256
|
+
|
|
257
|
+
args = parser.parse_args()
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
rc = args.func(args)
|
|
261
|
+
except CliError as e:
|
|
262
|
+
if args.debug:
|
|
263
|
+
traceback.print_exc()
|
|
264
|
+
print(f"error: {e}", file=sys.stderr)
|
|
265
|
+
if e.hint:
|
|
266
|
+
print(f"hint: {e.hint}", file=sys.stderr)
|
|
267
|
+
raise SystemExit(e.exit_code)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
if args.debug:
|
|
270
|
+
traceback.print_exc()
|
|
271
|
+
print(f"error: {e}", file=sys.stderr)
|
|
272
|
+
raise SystemExit(EXIT_ERROR)
|
|
273
|
+
|
|
274
|
+
raise SystemExit(rc)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!-- noop invocation for conjure command -->
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
task: add tests for existing code.
|
|
2
|
+
|
|
3
|
+
## inputs
|
|
4
|
+
|
|
5
|
+
- $ARGUMENTS (required): file path, function name, class name, or module to test.
|
|
6
|
+
|
|
7
|
+
## preconditions
|
|
8
|
+
|
|
9
|
+
STOP and ask if:
|
|
10
|
+
- the target is unclear or cannot be located
|
|
11
|
+
- you're unsure what behavior to test
|
|
12
|
+
- the code has no clear contract or expected behavior
|
|
13
|
+
|
|
14
|
+
before starting:
|
|
15
|
+
- read the code to understand its contract
|
|
16
|
+
- check if tests already exist (don't duplicate)
|
|
17
|
+
- identify the testing framework used in the project
|
|
18
|
+
|
|
19
|
+
## constraints
|
|
20
|
+
|
|
21
|
+
- **match project conventions**: use the same test framework, file locations, and naming patterns.
|
|
22
|
+
- **no behavior changes**: you're adding tests, not fixing bugs (note bugs separately).
|
|
23
|
+
- **minimal mocking**: only mock external dependencies; prefer real objects when possible.
|
|
24
|
+
|
|
25
|
+
## what to test
|
|
26
|
+
|
|
27
|
+
write tests covering:
|
|
28
|
+
1. **happy path**: the main success scenario
|
|
29
|
+
2. **edge case**: boundary conditions, empty inputs, large inputs
|
|
30
|
+
3. **error case**: invalid inputs, expected failures
|
|
31
|
+
|
|
32
|
+
## test quality checklist
|
|
33
|
+
|
|
34
|
+
each test should be:
|
|
35
|
+
- [ ] **deterministic**: no flakiness from time, network, or random values
|
|
36
|
+
- [ ] **isolated**: can run independently of other tests
|
|
37
|
+
- [ ] **fast**: no unnecessary I/O or sleeps
|
|
38
|
+
- [ ] **readable**: clear what's being tested and why
|
|
39
|
+
- [ ] **focused**: one behavior per test
|
|
40
|
+
|
|
41
|
+
## steps
|
|
42
|
+
|
|
43
|
+
1. **locate**: find the code to test and understand its contract.
|
|
44
|
+
2. **check**: verify no duplicate tests exist.
|
|
45
|
+
3. **plan**: list the test cases you will write.
|
|
46
|
+
4. **write**: implement tests, preferring parametrized/table-driven style.
|
|
47
|
+
5. **run**: execute the test suite and confirm all tests pass.
|
|
48
|
+
|
|
49
|
+
## output
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
## unit under test
|
|
53
|
+
<file:function or class being tested>
|
|
54
|
+
|
|
55
|
+
## test cases
|
|
56
|
+
1. <test name>: <what it verifies>
|
|
57
|
+
2. <test name>: <what it verifies>
|
|
58
|
+
3. <test name>: <what it verifies>
|
|
59
|
+
|
|
60
|
+
## changes
|
|
61
|
+
<diff of new test file or additions>
|
|
62
|
+
|
|
63
|
+
## verification
|
|
64
|
+
<exact test command>
|
|
65
|
+
|
|
66
|
+
## notes (optional)
|
|
67
|
+
<bugs noticed, edge cases not covered, etc.>
|
|
68
|
+
```
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
task: bootstrap a new python project with modern tooling.
|
|
2
|
+
|
|
3
|
+
## inputs
|
|
4
|
+
|
|
5
|
+
- $1 `package_name` (required): name of the package (e.g., `myapp`)
|
|
6
|
+
- $2 `package_type` (required): `cli` or `lib`
|
|
7
|
+
- $3 `python_version` (optional): minimum python version (default: `3.10`)
|
|
8
|
+
- $4 `license` (optional): license identifier (e.g., `MIT`, `Apache-2.0`)
|
|
9
|
+
|
|
10
|
+
## preconditions
|
|
11
|
+
|
|
12
|
+
STOP and ask if:
|
|
13
|
+
- package_name is missing or invalid (not a valid python identifier)
|
|
14
|
+
- package_type is not `cli` or `lib`
|
|
15
|
+
- the target directory already exists (ask: abort, overwrite, or integrate?)
|
|
16
|
+
|
|
17
|
+
## what gets created
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
<package_name>/
|
|
21
|
+
├── pyproject.toml # modern python packaging with ruff, mypy, pytest
|
|
22
|
+
├── README.md # one-line description + quickstart
|
|
23
|
+
├── src/
|
|
24
|
+
│ └── <package_name>/
|
|
25
|
+
│ ├── __init__.py # version and public API
|
|
26
|
+
│ ├── main.py # (cli) entry point with argument parsing
|
|
27
|
+
│ └── core.py # (lib) core module placeholder
|
|
28
|
+
└── tests/
|
|
29
|
+
├── __init__.py
|
|
30
|
+
└── test_<package_name>.py # minimal test
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## steps
|
|
34
|
+
|
|
35
|
+
1. **validate**: confirm inputs are valid and directory doesn't conflict.
|
|
36
|
+
2. **create**: set up directory structure with src layout.
|
|
37
|
+
3. **configure**: create pyproject.toml with metadata and tool configs.
|
|
38
|
+
4. **readme**: create README.md with purpose and quickstart.
|
|
39
|
+
5. **code**: add minimal implementation with type hints.
|
|
40
|
+
6. **test**: add a minimal passing test.
|
|
41
|
+
7. **verify**: run format, lint, type check, and tests.
|
|
42
|
+
|
|
43
|
+
## pyproject.toml structure
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
[project]
|
|
47
|
+
name = "<package_name>"
|
|
48
|
+
version = "0.1.0"
|
|
49
|
+
description = "TODO: add description"
|
|
50
|
+
requires-python = ">=<python_version>"
|
|
51
|
+
license = "<license>" # if provided
|
|
52
|
+
dependencies = []
|
|
53
|
+
|
|
54
|
+
[project.optional-dependencies]
|
|
55
|
+
dev = ["ruff", "mypy", "pytest", "pytest-cov"]
|
|
56
|
+
|
|
57
|
+
[project.scripts] # cli only
|
|
58
|
+
<package_name> = "<package_name>.main:main"
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py<python_version_short>"
|
|
62
|
+
line-length = 88
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "<python_version>"
|
|
66
|
+
strict = true
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## acceptance criteria
|
|
73
|
+
|
|
74
|
+
all must pass:
|
|
75
|
+
```
|
|
76
|
+
ruff format --check .
|
|
77
|
+
ruff check .
|
|
78
|
+
mypy .
|
|
79
|
+
pytest -q
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## output
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
## project created
|
|
86
|
+
<package_name> (<package_type>)
|
|
87
|
+
|
|
88
|
+
## files
|
|
89
|
+
<list of files created with brief description>
|
|
90
|
+
|
|
91
|
+
## changes
|
|
92
|
+
<diff of all created files>
|
|
93
|
+
|
|
94
|
+
## setup
|
|
95
|
+
pip install -e ".[dev]"
|
|
96
|
+
|
|
97
|
+
## verification
|
|
98
|
+
ruff format --check . && ruff check . && mypy . && pytest -q
|
|
99
|
+
|
|
100
|
+
## next steps
|
|
101
|
+
<suggested next actions: add dependencies, implement features, publish, etc.>
|
|
102
|
+
```
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
task: bootstrap a new rust crate with best practices.
|
|
2
|
+
|
|
3
|
+
## inputs
|
|
4
|
+
|
|
5
|
+
- $1 `crate_name` (required): name of the crate (e.g., `myapp`)
|
|
6
|
+
- $2 `crate_type` (required): `bin` or `lib`
|
|
7
|
+
- $3 `msrv` (optional): minimum supported rust version (e.g., `1.75`)
|
|
8
|
+
- $4 `license` (optional): license identifier (e.g., `MIT`, `Apache-2.0`)
|
|
9
|
+
|
|
10
|
+
## preconditions
|
|
11
|
+
|
|
12
|
+
STOP and ask if:
|
|
13
|
+
- crate_name is missing or invalid (not a valid rust identifier)
|
|
14
|
+
- crate_type is not `bin` or `lib`
|
|
15
|
+
- the target directory already exists (ask: abort, overwrite, or integrate?)
|
|
16
|
+
|
|
17
|
+
## what gets created
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
<crate_name>/
|
|
21
|
+
├── Cargo.toml # with metadata, msrv, license if provided
|
|
22
|
+
├── README.md # one-line description + quickstart
|
|
23
|
+
├── src/
|
|
24
|
+
│ ├── main.rs # (bin) minimal main with error handling
|
|
25
|
+
│ └── lib.rs # (lib) minimal module with doc comment
|
|
26
|
+
└── tests/ # (lib only) integration test placeholder
|
|
27
|
+
└── integration.rs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## steps
|
|
31
|
+
|
|
32
|
+
1. **validate**: confirm inputs are valid and directory doesn't conflict.
|
|
33
|
+
2. **create**: run `cargo new <crate_name> --<crate_type>`.
|
|
34
|
+
3. **configure**: update Cargo.toml with metadata, msrv, and license.
|
|
35
|
+
4. **readme**: create README.md with purpose and quickstart.
|
|
36
|
+
5. **test**: add a minimal test if not present.
|
|
37
|
+
6. **verify**: run format, clippy, and tests.
|
|
38
|
+
|
|
39
|
+
## cargo.toml structure
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[package]
|
|
43
|
+
name = "<crate_name>"
|
|
44
|
+
version = "0.1.0"
|
|
45
|
+
edition = "2021"
|
|
46
|
+
rust-version = "<msrv>" # if provided
|
|
47
|
+
license = "<license>" # if provided
|
|
48
|
+
description = "TODO: add description"
|
|
49
|
+
|
|
50
|
+
[dependencies]
|
|
51
|
+
|
|
52
|
+
[dev-dependencies]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## acceptance criteria
|
|
56
|
+
|
|
57
|
+
all must pass:
|
|
58
|
+
```
|
|
59
|
+
cargo fmt --check
|
|
60
|
+
cargo clippy --all-targets --all-features -- -D warnings
|
|
61
|
+
cargo test --all-features
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## output
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
## crate created
|
|
68
|
+
<crate_name> (<crate_type>)
|
|
69
|
+
|
|
70
|
+
## files
|
|
71
|
+
<list of files created with brief description>
|
|
72
|
+
|
|
73
|
+
## changes
|
|
74
|
+
<diff of all created/modified files>
|
|
75
|
+
|
|
76
|
+
## verification
|
|
77
|
+
cargo fmt --check && cargo clippy --all-targets --all-features -- -D warnings && cargo test --all-features
|
|
78
|
+
|
|
79
|
+
## next steps
|
|
80
|
+
<suggested next actions: add dependencies, implement features, etc.>
|
|
81
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
task: review code for quality, correctness, and maintainability.
|
|
2
|
+
|
|
3
|
+
## inputs
|
|
4
|
+
|
|
5
|
+
- $ARGUMENTS (optional): file path, function name, or scope to review. if empty, review recent changes or staged diff.
|
|
6
|
+
|
|
7
|
+
## preconditions
|
|
8
|
+
|
|
9
|
+
if no target is specified and no recent changes exist:
|
|
10
|
+
- ask what to review
|
|
11
|
+
- do not review arbitrary code
|
|
12
|
+
|
|
13
|
+
## review checklist
|
|
14
|
+
|
|
15
|
+
examine the code for:
|
|
16
|
+
|
|
17
|
+
**correctness**
|
|
18
|
+
- does it do what it claims to do?
|
|
19
|
+
- are edge cases handled?
|
|
20
|
+
- are error conditions handled properly?
|
|
21
|
+
- are there potential null/undefined issues?
|
|
22
|
+
|
|
23
|
+
**clarity**
|
|
24
|
+
- is the code easy to understand?
|
|
25
|
+
- are names descriptive and consistent?
|
|
26
|
+
- is the logic straightforward or needlessly complex?
|
|
27
|
+
- are there comments where needed (and only where needed)?
|
|
28
|
+
|
|
29
|
+
**maintainability**
|
|
30
|
+
- is the code modular and testable?
|
|
31
|
+
- does it follow project conventions?
|
|
32
|
+
- would a new team member understand it?
|
|
33
|
+
- is there duplication that should be extracted?
|
|
34
|
+
|
|
35
|
+
**performance** (only if relevant)
|
|
36
|
+
- are there obvious inefficiencies?
|
|
37
|
+
- is there unnecessary work in hot paths?
|
|
38
|
+
|
|
39
|
+
## how to give feedback
|
|
40
|
+
|
|
41
|
+
- **be specific**: reference file:line, quote the code
|
|
42
|
+
- **be actionable**: say what to change, not just what's wrong
|
|
43
|
+
- **be proportionate**: don't nitpick style in a bugfix review
|
|
44
|
+
- **acknowledge good work**: note well-written code too
|
|
45
|
+
|
|
46
|
+
## severity levels
|
|
47
|
+
|
|
48
|
+
- **blocker**: must fix before merge (bugs, security issues, data loss risks)
|
|
49
|
+
- **major**: should fix before merge (design issues, missing tests, unclear logic)
|
|
50
|
+
- **minor**: consider fixing (style, naming, minor improvements)
|
|
51
|
+
- **nit**: optional, take or leave (personal preferences)
|
|
52
|
+
|
|
53
|
+
## output
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
## scope
|
|
57
|
+
<what was reviewed: files, functions, commit range>
|
|
58
|
+
|
|
59
|
+
## summary
|
|
60
|
+
<1-2 sentence overall assessment>
|
|
61
|
+
|
|
62
|
+
## findings
|
|
63
|
+
|
|
64
|
+
### blockers
|
|
65
|
+
- [file:line] <issue>
|
|
66
|
+
- suggestion: <how to fix>
|
|
67
|
+
|
|
68
|
+
### major
|
|
69
|
+
- [file:line] <issue>
|
|
70
|
+
- suggestion: <how to fix>
|
|
71
|
+
|
|
72
|
+
### minor
|
|
73
|
+
- [file:line] <issue>
|
|
74
|
+
|
|
75
|
+
### positive
|
|
76
|
+
- <what was done well>
|
|
77
|
+
|
|
78
|
+
## verdict
|
|
79
|
+
<approve / request changes / needs discussion>
|
|
80
|
+
```
|