@prateek_ai/agents-maker 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROMPT_TEMPLATE.md +143 -0
- package/README.md +92 -25
- package/agents/brain.md +90 -0
- package/agents/orchestrator.md +24 -3
- package/agents/planpro.md +91 -0
- package/bin/cli.js +40 -4
- package/claude/agents/architect.md +182 -0
- package/claude/agents/brain.md +97 -0
- package/claude/agents/code.md +185 -0
- package/claude/agents/compress.md +233 -0
- package/claude/agents/execute.md +164 -0
- package/claude/agents/orchestrate.md +434 -0
- package/claude/agents/planpro.md +98 -0
- package/claude/agents/review.md +141 -0
- package/claude/agents/ui.md +154 -0
- package/claude/agents/ux.md +151 -0
- package/claude/commands/architect.md +15 -0
- package/claude/commands/brain.md +15 -0
- package/claude/commands/code.md +15 -0
- package/claude/commands/compress.md +15 -0
- package/claude/commands/execute.md +15 -0
- package/claude/commands/orchestrate.md +15 -0
- package/claude/commands/planpro.md +15 -0
- package/claude/commands/review.md +15 -0
- package/claude/commands/ui.md +15 -0
- package/claude/commands/ux.md +15 -0
- package/config/agents.yaml +56 -0
- package/context_loaders/project_summary.py +5 -0
- package/docs/architecture.md +318 -0
- package/docs/domains.md +154 -0
- package/docs/workflows.md +548 -0
- package/examples/generic_project_lifecycle.md +518 -0
- package/examples/proof/README.md +138 -0
- package/examples/proof/implement-a-python-function-that-parses-an-iso-8/grade.md +14 -0
- package/examples/proof/implement-a-python-function-that-parses-an-iso-8/naive_output.md +211 -0
- package/examples/proof/implement-a-python-function-that-parses-an-iso-8/naive_prompt.txt +1 -0
- package/examples/proof/implement-a-python-function-that-parses-an-iso-8/structured_output.md +210 -0
- package/examples/proof/implement-a-python-function-that-parses-an-iso-8/structured_user.txt +35 -0
- package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/grade.md +14 -0
- package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/naive_output.md +274 -0
- package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/naive_prompt.txt +1 -0
- package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/structured_output.md +127 -0
- package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/structured_user.txt +35 -0
- package/package.json +10 -2
- package/platforms/antigravity.md +195 -0
- package/platforms/claude.md +305 -0
- package/platforms/openai.md +333 -0
- package/skills/analyze_repo.md +86 -86
- package/token_optimization/compressor.py +4 -7
- package/tools/_core.py +102 -0
- package/tools/compare_prompts.py +196 -0
- package/tools/generate_claude_agents.py +162 -0
- package/tools/generate_claude_md.py +14 -80
- package/tools/generate_platform_configs.py +26 -36
- package/tools/generate_prompt.py +79 -79
- package/tools/grade_proof.py +118 -0
- package/tools/init_project.py +11 -58
- package/tools/routing.py +103 -0
- package/tools/test_kit.py +392 -363
- package/tools/validate_kit.py +15 -43
- package/context_loaders/__pycache__/__init__.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/file_chunker.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/project_summary.cpython-314.pyc +0 -0
- package/context_loaders/__pycache__/repo_tree.cpython-314.pyc +0 -0
- package/token_optimization/__pycache__/compressor.cpython-314.pyc +0 -0
- package/tools/__pycache__/domain_utils.cpython-314.pyc +0 -0
- package/tools/__pycache__/generate_claude_md.cpython-314.pyc +0 -0
- package/tools/__pycache__/validate_kit.cpython-314.pyc +0 -0
package/skills/analyze_repo.md
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
# Skill: analyze_repo
|
|
2
|
-
|
|
3
|
-
## Description
|
|
4
|
-
|
|
5
|
-
Walk a repository's file tree and produce a compact, structured summary of the project: detected stack, primary services or modules, main entrypoints, test structure, and key configuration files. Used by the Orchestrator and Architect Agent to build the Project State block at the start of a session.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## When to invoke
|
|
10
|
-
|
|
11
|
-
- Session start, when no project summary is available.
|
|
12
|
-
- When routing a task that requires understanding the broader project structure beyond the provided snippets.
|
|
13
|
-
- When the Architect Agent needs a service map before designing a new component.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Input expectations
|
|
18
|
-
|
|
19
|
-
| Input | Required | Description |
|
|
20
|
-
|---|---|---|
|
|
21
|
-
| `repo_path` | Yes | Root path of the repository |
|
|
22
|
-
| `filter_paths` | No | List of subdirectory prefixes to include (e.g., `src/`, `app/`, `services/`) |
|
|
23
|
-
| `exclude_patterns` | No | Patterns to exclude (e.g., `node_modules/`, `__pycache__/`, `.git/`) |
|
|
24
|
-
| `max_depth` | No | Maximum directory depth to traverse (default: 4) |
|
|
25
|
-
|
|
26
|
-
In a conversational context, the user provides these via the `context_loaders/repo_tree.py` and `context_loaders/project_summary.py` scripts, and pastes the output.
|
|
27
|
-
|
|
28
|
-
**If required input is missing:**
|
|
29
|
-
- `repo_path` — if no path is provided and no repo context has been pasted, ask: "Please paste the output of `python agents-maker/context_loaders/repo_tree.py` or describe your project's directory structure." Do not produce a summary from nothing.
|
|
30
|
-
- `filter_paths` — default to scanning the entire repo up to `max_depth`.
|
|
31
|
-
- `exclude_patterns` — default to excluding `node_modules/`, `__pycache__/`, `.git/`, `dist/`, `build/`.
|
|
32
|
-
- `max_depth` — default to 4.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Output format
|
|
37
|
-
|
|
38
|
-
The skill produces a structured text block:
|
|
39
|
-
|
|
40
|
-
```
|
|
41
|
-
## Project Summary
|
|
42
|
-
|
|
43
|
-
**Stack**: <language(s), runtime version(s), primary framework(s)>
|
|
44
|
-
**Build tool**: <e.g., pip + setuptools, npm + webpack, gradle>
|
|
45
|
-
**Test framework**: <e.g., pytest, jest, JUnit>
|
|
46
|
-
**Containerization**: <Docker | none | Kubernetes manifests present>
|
|
47
|
-
|
|
48
|
-
## Services / Modules
|
|
49
|
-
|
|
50
|
-
| Name | Path | Responsibility |
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| <name> | <path> | <one-line description> |
|
|
53
|
-
|
|
54
|
-
## Main Entrypoints
|
|
55
|
-
|
|
56
|
-
| File | Purpose |
|
|
57
|
-
|---|---|
|
|
58
|
-
| <path> | <what it starts or exports> |
|
|
59
|
-
|
|
60
|
-
## Key Config Files
|
|
61
|
-
|
|
62
|
-
| File | Purpose |
|
|
63
|
-
|---|---|
|
|
64
|
-
| <path> | <what it configures> |
|
|
65
|
-
|
|
66
|
-
## Test Structure
|
|
67
|
-
|
|
68
|
-
| Path | Type | Coverage scope |
|
|
69
|
-
|---|---|---|
|
|
70
|
-
| <path> | unit | integration | e2e | <scope> |
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## Token cost tier
|
|
76
|
-
|
|
77
|
-
**Medium.** Involves reading file tree and inspecting key files. Output is typically 300–600 tokens.
|
|
78
|
-
|
|
79
|
-
Compression hint: the output is already compact. Do not summarize it further — it is the basis for all other context in the session.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
## Notes
|
|
84
|
-
|
|
85
|
-
- This skill is implemented as `context_loaders/project_summary.py`. In agent sessions without tool access, the user runs it locally and pastes the output.
|
|
86
|
-
- If the repo has no recognizable structure, return the raw tree truncated at `max_depth` and note: "Could not detect stack — please specify language and framework manually."
|
|
1
|
+
# Skill: analyze_repo
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
Walk a repository's file tree and produce a compact, structured summary of the project: detected stack, primary services or modules, main entrypoints, test structure, and key configuration files. Used by the Orchestrator and Architect Agent to build the Project State block at the start of a session.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## When to invoke
|
|
10
|
+
|
|
11
|
+
- Session start, when no project summary is available.
|
|
12
|
+
- When routing a task that requires understanding the broader project structure beyond the provided snippets.
|
|
13
|
+
- When the Architect Agent needs a service map before designing a new component.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Input expectations
|
|
18
|
+
|
|
19
|
+
| Input | Required | Description |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `repo_path` | Yes | Root path of the repository |
|
|
22
|
+
| `filter_paths` | No | List of subdirectory prefixes to include (e.g., `src/`, `app/`, `services/`) |
|
|
23
|
+
| `exclude_patterns` | No | Patterns to exclude (e.g., `node_modules/`, `__pycache__/`, `.git/`) |
|
|
24
|
+
| `max_depth` | No | Maximum directory depth to traverse (default: 4) |
|
|
25
|
+
|
|
26
|
+
In a conversational context, the user provides these via the `context_loaders/repo_tree.py` and `context_loaders/project_summary.py` scripts, and pastes the output.
|
|
27
|
+
|
|
28
|
+
**If required input is missing:**
|
|
29
|
+
- `repo_path` — if no path is provided and no repo context has been pasted, ask: "Please paste the output of `python agents-maker/context_loaders/repo_tree.py` or describe your project's directory structure." Do not produce a summary from nothing.
|
|
30
|
+
- `filter_paths` — default to scanning the entire repo up to `max_depth`.
|
|
31
|
+
- `exclude_patterns` — default to excluding `node_modules/`, `__pycache__/`, `.git/`, `dist/`, `build/`.
|
|
32
|
+
- `max_depth` — default to 4.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Output format
|
|
37
|
+
|
|
38
|
+
The skill produces a structured text block:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
## Project Summary
|
|
42
|
+
|
|
43
|
+
**Stack**: <language(s), runtime version(s), primary framework(s)>
|
|
44
|
+
**Build tool**: <e.g., pip + setuptools, npm + webpack, gradle>
|
|
45
|
+
**Test framework**: <e.g., pytest, jest, JUnit>
|
|
46
|
+
**Containerization**: <Docker | none | Kubernetes manifests present>
|
|
47
|
+
|
|
48
|
+
## Services / Modules
|
|
49
|
+
|
|
50
|
+
| Name | Path | Responsibility |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| <name> | <path> | <one-line description> |
|
|
53
|
+
|
|
54
|
+
## Main Entrypoints
|
|
55
|
+
|
|
56
|
+
| File | Purpose |
|
|
57
|
+
|---|---|
|
|
58
|
+
| <path> | <what it starts or exports> |
|
|
59
|
+
|
|
60
|
+
## Key Config Files
|
|
61
|
+
|
|
62
|
+
| File | Purpose |
|
|
63
|
+
|---|---|
|
|
64
|
+
| <path> | <what it configures> |
|
|
65
|
+
|
|
66
|
+
## Test Structure
|
|
67
|
+
|
|
68
|
+
| Path | Type | Coverage scope |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| <path> | unit | integration | e2e | <scope> |
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Token cost tier
|
|
76
|
+
|
|
77
|
+
**Medium.** Involves reading file tree and inspecting key files. Output is typically 300–600 tokens.
|
|
78
|
+
|
|
79
|
+
Compression hint: the output is already compact. Do not summarize it further — it is the basis for all other context in the session.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Notes
|
|
84
|
+
|
|
85
|
+
- This skill is implemented as `context_loaders/project_summary.py`. In agent sessions without tool access, the user runs it locally and pastes the output.
|
|
86
|
+
- If the repo has no recognizable structure, return the raw tree truncated at `max_depth` and note: "Could not detect stack — please specify language and framework manually."
|
|
@@ -126,11 +126,13 @@ class RelevanceFilter:
|
|
|
126
126
|
See token_optimization/relevance_filter.md for the full scoring model.
|
|
127
127
|
"""
|
|
128
128
|
|
|
129
|
+
# Weights sum to 1.0. (A prior "recency" weight was removed: it had no data
|
|
130
|
+
# source and was hardwired to 0.0, so it silently contributed nothing; its
|
|
131
|
+
# budget is folded into lexical_overlap, the primary textual signal.)
|
|
129
132
|
weights = {
|
|
130
|
-
"lexical_overlap": 0.
|
|
133
|
+
"lexical_overlap": 0.45,
|
|
131
134
|
"direct_reference": 0.30,
|
|
132
135
|
"symbol_mention": 0.20,
|
|
133
|
-
"recency": 0.10,
|
|
134
136
|
"structural_importance": 0.05,
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -177,15 +179,10 @@ class RelevanceFilter:
|
|
|
177
179
|
symbol = self._symbol_mention(content_lower, raw_query)
|
|
178
180
|
structural = 1.0 if any(p in path_lower for p in self._structural_patterns) else 0.0
|
|
179
181
|
|
|
180
|
-
# recency: caller must set f.relevance_score > 0 before calling this method
|
|
181
|
-
# to use it as a recency hint; otherwise defaults to 0.
|
|
182
|
-
recency = 0.0
|
|
183
|
-
|
|
184
182
|
return (
|
|
185
183
|
self.weights["lexical_overlap"] * lexical
|
|
186
184
|
+ self.weights["direct_reference"] * direct
|
|
187
185
|
+ self.weights["symbol_mention"] * symbol
|
|
188
|
-
+ self.weights["recency"] * recency
|
|
189
186
|
+ self.weights["structural_importance"] * structural
|
|
190
187
|
)
|
|
191
188
|
|
package/tools/_core.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/_core.py
|
|
3
|
+
|
|
4
|
+
Shared low-level primitives for the agents-maker tools: YAML loading,
|
|
5
|
+
crash-safe atomic writes, source-hash computation, and shell-invocation
|
|
6
|
+
formatting. Centralizing these removes the copy-pasted helpers that were
|
|
7
|
+
previously duplicated across init_project.py, generate_prompt.py,
|
|
8
|
+
generate_claude_md.py, generate_platform_configs.py, and validate_kit.py.
|
|
9
|
+
|
|
10
|
+
Behavior is intentionally identical to the helpers it replaces.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# agents-maker/ (parent of tools/)
|
|
24
|
+
KIT_DIR = Path(__file__).resolve().parent.parent
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import yaml
|
|
28
|
+
except ImportError: # pragma: no cover - environment guard
|
|
29
|
+
print("[ERROR] pyyaml is required: pip install pyyaml", file=sys.stderr)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# YAML loading
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def load_yaml(path: Path) -> dict:
|
|
38
|
+
"""Load a YAML file, returning {} on missing file and logging parse/permission errors."""
|
|
39
|
+
try:
|
|
40
|
+
with open(path, encoding="utf-8") as f:
|
|
41
|
+
return yaml.safe_load(f) or {}
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
return {}
|
|
44
|
+
except yaml.YAMLError as e:
|
|
45
|
+
print(f"[ERROR] YAML parse error in {path}: {e}", file=sys.stderr)
|
|
46
|
+
return {}
|
|
47
|
+
except PermissionError:
|
|
48
|
+
print(f"[ERROR] Permission denied reading {path}", file=sys.stderr)
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Atomic writes (write to temp then os.replace — crash-safe)
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def atomic_write(path: Path, content: str) -> None:
|
|
57
|
+
"""Atomically write text to path (temp file + os.replace)."""
|
|
58
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
with tempfile.NamedTemporaryFile(
|
|
60
|
+
"w", dir=path.parent, delete=False, suffix=".tmp", encoding="utf-8"
|
|
61
|
+
) as f:
|
|
62
|
+
f.write(content)
|
|
63
|
+
tmp = f.name
|
|
64
|
+
os.replace(tmp, path)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def atomic_write_yaml(path: Path, data: dict) -> None:
|
|
68
|
+
"""Atomically dump a dict to a YAML file (temp file + os.replace)."""
|
|
69
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
with tempfile.NamedTemporaryFile(
|
|
71
|
+
"w", dir=path.parent, delete=False, suffix=".tmp", encoding="utf-8"
|
|
72
|
+
) as f:
|
|
73
|
+
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
74
|
+
tmp = f.name
|
|
75
|
+
os.replace(tmp, path)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
# Source hash — lets validate_kit.py detect a stale system_prompt.md
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def source_hash(kit_dir: Path = KIT_DIR) -> str:
|
|
83
|
+
"""SHA-256 (first 16 hex chars) over all agent + skill markdown, line-ending normalized."""
|
|
84
|
+
h = hashlib.sha256()
|
|
85
|
+
agent_files = sorted((kit_dir / "agents").glob("*.md")) if (kit_dir / "agents").is_dir() else []
|
|
86
|
+
skill_files = sorted((kit_dir / "skills").glob("*.md")) if (kit_dir / "skills").is_dir() else []
|
|
87
|
+
for path in agent_files + skill_files:
|
|
88
|
+
try:
|
|
89
|
+
h.update(path.read_bytes().replace(b"\r\n", b"\n"))
|
|
90
|
+
except (OSError, PermissionError):
|
|
91
|
+
pass
|
|
92
|
+
return h.hexdigest()[:16]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Shell-invocation formatting
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def py_invocation(kit_rel: str, tool: str) -> str:
|
|
100
|
+
"""Return a shell-safe 'python ...' invocation, quoting the path when it contains spaces."""
|
|
101
|
+
path = f"{kit_rel}/tools/{tool}"
|
|
102
|
+
return f'python "{path}"' if " " in path else f"python {path}"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
compare_prompts.py — Proof-of-value harness for agents-maker.
|
|
4
|
+
|
|
5
|
+
For a given task it builds TWO prompts and runs BOTH through the same model,
|
|
6
|
+
so you can see what the structuring actually changes:
|
|
7
|
+
|
|
8
|
+
1. NAIVE — the raw task, exactly as a user would type it into a chat box.
|
|
9
|
+
2. STRUCTURED — the agents-maker prompt (system prompt + domain routing +
|
|
10
|
+
project context + Companion Mode), via generate_prompt.py --full.
|
|
11
|
+
|
|
12
|
+
Outputs (prompts + raw model responses) are written to examples/proof/<slug>/
|
|
13
|
+
so the comparison is reproducible and auditable — no cherry-picking.
|
|
14
|
+
|
|
15
|
+
Model runner
|
|
16
|
+
------------
|
|
17
|
+
By default it shells out to the Claude Code CLI in non-interactive mode
|
|
18
|
+
(`claude -p`), which reuses your existing auth (no API key needed). Override
|
|
19
|
+
with --runner to use any command that reads a prompt on stdin and prints the
|
|
20
|
+
completion on stdout, e.g.:
|
|
21
|
+
|
|
22
|
+
python tools/compare_prompts.py "..." --runner "llm -m gpt-4o"
|
|
23
|
+
|
|
24
|
+
Usage
|
|
25
|
+
-----
|
|
26
|
+
python tools/compare_prompts.py "add rate limiting to the auth service"
|
|
27
|
+
python tools/compare_prompts.py "[domain: ops_process] write a Redis failover runbook"
|
|
28
|
+
python tools/compare_prompts.py "write a launch tweet" --skip-run # build prompts only
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import re
|
|
37
|
+
import subprocess
|
|
38
|
+
import sys
|
|
39
|
+
import tempfile
|
|
40
|
+
import urllib.request
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
44
|
+
KIT_DIR = SCRIPT_DIR.parent
|
|
45
|
+
PROOF_DIR = KIT_DIR / "examples" / "proof"
|
|
46
|
+
|
|
47
|
+
_SEP = "=" * 60
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _slug(task: str) -> str:
|
|
51
|
+
s = re.sub(r"\[domain:[^\]]*\]", "", task).strip().lower()
|
|
52
|
+
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
|
|
53
|
+
return (s[:48] or "task").rstrip("-")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_structured_prompt(task: str, full: bool = True) -> str:
|
|
57
|
+
"""Run generate_prompt.py and extract the pasteable prompt body.
|
|
58
|
+
|
|
59
|
+
full=True inlines the whole system prompt (single-message / free-tier paste).
|
|
60
|
+
full=False returns only the context+task+routing block — use it as the `user`
|
|
61
|
+
message when the system prompt is supplied separately in the `system` field.
|
|
62
|
+
"""
|
|
63
|
+
cmd = [sys.executable, str(SCRIPT_DIR / "generate_prompt.py"), task]
|
|
64
|
+
if full:
|
|
65
|
+
cmd.append("--full")
|
|
66
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
67
|
+
if proc.returncode != 0:
|
|
68
|
+
raise RuntimeError(f"generate_prompt.py failed: {proc.stderr[:300]}")
|
|
69
|
+
# Output is: SEP / header / SEP / <body> / SEP / footer / SEP
|
|
70
|
+
parts = proc.stdout.split(_SEP)
|
|
71
|
+
if len(parts) < 4:
|
|
72
|
+
raise RuntimeError("Unexpected generate_prompt.py output format.")
|
|
73
|
+
return parts[2].strip()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run_anthropic(system: str | None, user: str, model: str, max_tokens: int = 3000) -> str:
|
|
77
|
+
"""Call the Anthropic Messages API with proper role separation. Key from env."""
|
|
78
|
+
key = os.environ.get("ANTHROPIC_API_KEY")
|
|
79
|
+
if not key:
|
|
80
|
+
raise RuntimeError("ANTHROPIC_API_KEY is not set.")
|
|
81
|
+
body: dict = {"model": model, "max_tokens": max_tokens, "messages": [{"role": "user", "content": user}]}
|
|
82
|
+
if system:
|
|
83
|
+
# Mark the (large, stable) system prompt as cacheable: the first call writes
|
|
84
|
+
# the cache, later calls read it at ~0.1x input cost.
|
|
85
|
+
body["system"] = [{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}]
|
|
86
|
+
req = urllib.request.Request(
|
|
87
|
+
"https://api.anthropic.com/v1/messages",
|
|
88
|
+
data=json.dumps(body).encode("utf-8"),
|
|
89
|
+
method="POST",
|
|
90
|
+
headers={"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"},
|
|
91
|
+
)
|
|
92
|
+
try:
|
|
93
|
+
with urllib.request.urlopen(req, timeout=180) as resp:
|
|
94
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
95
|
+
except urllib.error.HTTPError as e:
|
|
96
|
+
return f"[api error {e.code}] {e.read().decode('utf-8', 'replace')[:400]}"
|
|
97
|
+
u = data.get("usage", {})
|
|
98
|
+
print(
|
|
99
|
+
f" [usage] input={u.get('input_tokens',0)} "
|
|
100
|
+
f"cache_write={u.get('cache_creation_input_tokens',0)} "
|
|
101
|
+
f"cache_read={u.get('cache_read_input_tokens',0)} "
|
|
102
|
+
f"output={u.get('output_tokens',0)}",
|
|
103
|
+
file=sys.stderr,
|
|
104
|
+
)
|
|
105
|
+
return "".join(b.get("text", "") for b in data.get("content", [])).strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def run_model(prompt: str, runner: str, cwd: str) -> str:
|
|
109
|
+
"""Feed a prompt to the model runner (stdin) and return the completion text.
|
|
110
|
+
|
|
111
|
+
Runs in an isolated empty `cwd` so an agent-style runner (e.g. claude -p)
|
|
112
|
+
answers the prompt as text instead of inspecting the current repository —
|
|
113
|
+
otherwise the comparison is confounded by whatever project it is run from.
|
|
114
|
+
"""
|
|
115
|
+
cmd = runner.split() + ([] if runner.split()[0] != "claude" else ["-p"])
|
|
116
|
+
proc = subprocess.run(
|
|
117
|
+
cmd, input=prompt, capture_output=True, text=True,
|
|
118
|
+
encoding="utf-8", errors="replace", cwd=cwd,
|
|
119
|
+
)
|
|
120
|
+
if proc.returncode != 0:
|
|
121
|
+
return f"[runner error rc={proc.returncode}]\n{proc.stderr[:500]}"
|
|
122
|
+
return proc.stdout.strip()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
127
|
+
ap = argparse.ArgumentParser(description="Compare a naive vs agents-maker structured prompt on the same model.")
|
|
128
|
+
ap.add_argument("task", help="The task to test (supports a leading [domain: X] prefix).")
|
|
129
|
+
ap.add_argument("--runner", default="claude",
|
|
130
|
+
help='CLI mode: model command reading stdin, printing stdout (default: "claude" -> claude -p).')
|
|
131
|
+
ap.add_argument("--api", action="store_true",
|
|
132
|
+
help="Use the Anthropic Messages API with proper system/user role separation (needs ANTHROPIC_API_KEY).")
|
|
133
|
+
ap.add_argument("--model", default="claude-haiku-4-5-20251001", help="Model id for --api mode.")
|
|
134
|
+
ap.add_argument("--skip-run", action="store_true", help="Only build the two prompts; do not call the model.")
|
|
135
|
+
ap.add_argument("--out", default=None, help="Output directory (default: examples/proof/<slug>/).")
|
|
136
|
+
args = ap.parse_args()
|
|
137
|
+
|
|
138
|
+
out_dir = Path(args.out) if args.out else PROOF_DIR / _slug(args.task)
|
|
139
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
|
|
141
|
+
if args.api:
|
|
142
|
+
# Role-separated: task-scoped system prompt in `system`, task+context as `user`.
|
|
143
|
+
sp = subprocess.run(
|
|
144
|
+
[sys.executable, str(SCRIPT_DIR / "generate_prompt.py"), args.task, "--system-only"],
|
|
145
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
|
146
|
+
)
|
|
147
|
+
if sp.returncode != 0:
|
|
148
|
+
raise RuntimeError(f"generate_prompt --system-only failed: {sp.stderr[:300]}")
|
|
149
|
+
system_prompt = sp.stdout.strip()
|
|
150
|
+
naive = args.task
|
|
151
|
+
structured_user = build_structured_prompt(args.task, full=False)
|
|
152
|
+
(out_dir / "naive_prompt.txt").write_text(naive + "\n", encoding="utf-8")
|
|
153
|
+
(out_dir / "structured_user.txt").write_text(structured_user + "\n", encoding="utf-8")
|
|
154
|
+
print(f"[built] {out_dir} (api mode, model={args.model})")
|
|
155
|
+
print(f" naive user: {len(naive)} chars | structured system: {len(system_prompt)} + user: {len(structured_user)} chars")
|
|
156
|
+
if args.skip_run:
|
|
157
|
+
print("[skip-run] prompts written; model not called.")
|
|
158
|
+
return
|
|
159
|
+
print("[run] naive (no system prompt)...")
|
|
160
|
+
naive_out = run_anthropic(None, naive, args.model)
|
|
161
|
+
(out_dir / "naive_output.md").write_text(naive_out + "\n", encoding="utf-8")
|
|
162
|
+
print("[run] structured (system=system_prompt.md, user=task+context)...")
|
|
163
|
+
structured_out = run_anthropic(system_prompt, structured_user, args.model)
|
|
164
|
+
(out_dir / "structured_output.md").write_text(structured_out + "\n", encoding="utf-8")
|
|
165
|
+
print(f"[done] outputs written to {out_dir}")
|
|
166
|
+
print(f" naive_output: {len(naive_out)} chars | structured_output: {len(structured_out)} chars")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
naive = args.task
|
|
170
|
+
structured = build_structured_prompt(args.task, full=True)
|
|
171
|
+
|
|
172
|
+
(out_dir / "naive_prompt.txt").write_text(naive + "\n", encoding="utf-8")
|
|
173
|
+
(out_dir / "structured_prompt.txt").write_text(structured + "\n", encoding="utf-8")
|
|
174
|
+
print(f"[built] {out_dir}")
|
|
175
|
+
print(f" naive: {len(naive)} chars | structured: {len(structured)} chars")
|
|
176
|
+
|
|
177
|
+
if args.skip_run:
|
|
178
|
+
print("[skip-run] prompts written; model not called.")
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
sandbox = tempfile.mkdtemp(prefix="am-proof-")
|
|
182
|
+
|
|
183
|
+
print(f"[run] naive via '{args.runner}' (isolated cwd)...")
|
|
184
|
+
naive_out = run_model(naive, args.runner, sandbox)
|
|
185
|
+
(out_dir / "naive_output.md").write_text(naive_out + "\n", encoding="utf-8")
|
|
186
|
+
|
|
187
|
+
print(f"[run] structured via '{args.runner}' (isolated cwd)...")
|
|
188
|
+
structured_out = run_model(structured, args.runner, sandbox)
|
|
189
|
+
(out_dir / "structured_output.md").write_text(structured_out + "\n", encoding="utf-8")
|
|
190
|
+
|
|
191
|
+
print(f"[done] outputs written to {out_dir}")
|
|
192
|
+
print(f" naive_output: {len(naive_out)} chars | structured_output: {len(structured_out)} chars")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == "__main__":
|
|
196
|
+
main()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
generate_claude_agents.py — Emit Claude Code subagents + slash commands.
|
|
4
|
+
|
|
5
|
+
Turns the agents-maker roster into native Claude Code files so a user can clone
|
|
6
|
+
the kit into their project and immediately invoke named agents:
|
|
7
|
+
|
|
8
|
+
/brain brainstorm the whole project (options + trade-offs + recommendation)
|
|
9
|
+
/planpro produce the best-possible implementation plan
|
|
10
|
+
/architect /code /execute /ui /ux /review /orchestrate /compress
|
|
11
|
+
|
|
12
|
+
For each agent it writes:
|
|
13
|
+
<dest>/.claude/agents/<name>.md — subagent (frontmatter + the agent spec body)
|
|
14
|
+
<dest>/.claude/commands/<name>.md — slash command that invokes the subagent
|
|
15
|
+
|
|
16
|
+
Non-destructive by default: existing same-named files are skipped (never clobber
|
|
17
|
+
a user's own agents/commands). Use --force to overwrite.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python agents-maker/tools/generate_claude_agents.py # -> <project>/.claude
|
|
21
|
+
python agents-maker/tools/generate_claude_agents.py --project /path # -> /path/.claude
|
|
22
|
+
python agents-maker/tools/generate_claude_agents.py --template # -> agents-maker/claude (shipped copy)
|
|
23
|
+
python agents-maker/tools/generate_claude_agents.py --dry-run
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR = Path(__file__).resolve().parent # agents-maker/tools/
|
|
35
|
+
KIT_DIR = SCRIPT_DIR.parent # agents-maker/
|
|
36
|
+
sys.path.insert(0, str(KIT_DIR))
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from tools._core import atomic_write, load_yaml
|
|
40
|
+
except ImportError:
|
|
41
|
+
from _core import atomic_write, load_yaml
|
|
42
|
+
|
|
43
|
+
# Full autonomy: agents may read, edit, write, and run commands.
|
|
44
|
+
AGENT_TOOLS = "Read, Grep, Glob, Edit, Write, Bash"
|
|
45
|
+
|
|
46
|
+
# command name -> agent id (filename stem in agents/). The command/subagent use
|
|
47
|
+
# the short, clean name; the body comes from the (possibly _agent-suffixed) spec.
|
|
48
|
+
COMMANDS: dict[str, str] = {
|
|
49
|
+
"brain": "brain",
|
|
50
|
+
"planpro": "planpro",
|
|
51
|
+
"orchestrate": "orchestrator",
|
|
52
|
+
"architect": "architect_agent",
|
|
53
|
+
"code": "code_agent",
|
|
54
|
+
"execute": "execution_agent",
|
|
55
|
+
"ui": "ui_agent",
|
|
56
|
+
"ux": "ux_agent",
|
|
57
|
+
"review": "reviewer_agent",
|
|
58
|
+
"compress": "compression_agent",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _flatten(text: str, limit: int = 300) -> str:
|
|
63
|
+
"""Collapse whitespace to a single line (YAML-frontmatter-safe) and cap length."""
|
|
64
|
+
one = " ".join(text.split())
|
|
65
|
+
return one[: limit - 1] + "…" if len(one) > limit else one
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _description(agent_id: str, agents_cfg: dict) -> str:
|
|
69
|
+
desc = _flatten(str(agents_cfg.get(agent_id, {}).get("description", "")))
|
|
70
|
+
return desc or f"agents-maker {agent_id} agent"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def build_subagent(cmd: str, agent_id: str, agents_cfg: dict, kit_dir: Path) -> str:
|
|
74
|
+
body = (kit_dir / "agents" / f"{agent_id}.md").read_text(encoding="utf-8").strip()
|
|
75
|
+
return (
|
|
76
|
+
"---\n"
|
|
77
|
+
f"name: {cmd}\n"
|
|
78
|
+
f"description: {_description(agent_id, agents_cfg)}\n"
|
|
79
|
+
f"tools: {AGENT_TOOLS}\n"
|
|
80
|
+
"model: inherit\n"
|
|
81
|
+
"---\n\n"
|
|
82
|
+
f"{body}\n"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_command(cmd: str, agent_id: str, agents_cfg: dict) -> str:
|
|
87
|
+
return (
|
|
88
|
+
"---\n"
|
|
89
|
+
f"description: {_description(agent_id, agents_cfg)}\n"
|
|
90
|
+
"---\n"
|
|
91
|
+
f"# /{cmd}\n\n"
|
|
92
|
+
"$ARGUMENTS\n\n"
|
|
93
|
+
"## Task\n"
|
|
94
|
+
f"Use the `{cmd}` subagent (agents-maker) to handle the request above.\n\n"
|
|
95
|
+
"CONTEXT:\n"
|
|
96
|
+
"- Project config, if present: `agents-maker/config/project.yaml` — read it for "
|
|
97
|
+
"domain, stack, and constraints before acting.\n"
|
|
98
|
+
"- User request: $ARGUMENTS\n\n"
|
|
99
|
+
f"Follow the `{cmd}` agent's output contract. If the request is a self-contained "
|
|
100
|
+
"task, deliver the finished artifact directly (Direct Task Mode) — do not ask for "
|
|
101
|
+
"project context you can infer. End with the [Companion] next-steps block.\n"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def generate(dest_claude: Path, kit_dir: Path, *, force: bool = False, dry_run: bool = False) -> tuple[list[str], list[str]]:
|
|
106
|
+
"""Write subagents + commands under dest_claude. Returns (written, skipped) rel paths."""
|
|
107
|
+
agents_cfg = load_yaml(kit_dir / "config" / "agents.yaml").get("agents", {})
|
|
108
|
+
written: list[str] = []
|
|
109
|
+
skipped: list[str] = []
|
|
110
|
+
|
|
111
|
+
for cmd, agent_id in COMMANDS.items():
|
|
112
|
+
targets = [
|
|
113
|
+
(dest_claude / "agents" / f"{cmd}.md", build_subagent(cmd, agent_id, agents_cfg, kit_dir)),
|
|
114
|
+
(dest_claude / "commands" / f"{cmd}.md", build_command(cmd, agent_id, agents_cfg)),
|
|
115
|
+
]
|
|
116
|
+
for path, content in targets:
|
|
117
|
+
rel = f"{path.parent.name}/{path.name}"
|
|
118
|
+
if path.exists() and not force:
|
|
119
|
+
skipped.append(rel)
|
|
120
|
+
continue
|
|
121
|
+
if dry_run:
|
|
122
|
+
written.append(rel)
|
|
123
|
+
continue
|
|
124
|
+
atomic_write(path, content)
|
|
125
|
+
written.append(rel)
|
|
126
|
+
|
|
127
|
+
return written, skipped
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main() -> None:
|
|
131
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
132
|
+
ap = argparse.ArgumentParser(description="Generate Claude Code subagents + slash commands from the agents-maker roster.")
|
|
133
|
+
ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
134
|
+
ap.add_argument("--project", help="Project root; writes <root>/.claude/ (default: parent of agents-maker/).")
|
|
135
|
+
ap.add_argument("--template", action="store_true", help="Write the shipped template copy to agents-maker/claude/ instead of a project's .claude/.")
|
|
136
|
+
ap.add_argument("--force", action="store_true", help="Overwrite existing files (default: skip existing).")
|
|
137
|
+
ap.add_argument("--dry-run", action="store_true", help="List what would be written without writing.")
|
|
138
|
+
args = ap.parse_args()
|
|
139
|
+
|
|
140
|
+
if args.template:
|
|
141
|
+
dest = KIT_DIR / "claude"
|
|
142
|
+
else:
|
|
143
|
+
root = Path(args.project).resolve() if args.project else KIT_DIR.parent
|
|
144
|
+
dest = root / ".claude"
|
|
145
|
+
|
|
146
|
+
written, skipped = generate(dest, KIT_DIR, force=args.force, dry_run=args.dry_run)
|
|
147
|
+
|
|
148
|
+
tag = "[dry-run] would write" if args.dry_run else "wrote"
|
|
149
|
+
print(f"{tag} {len(written)} file(s) under {dest}")
|
|
150
|
+
for r in written:
|
|
151
|
+
print(f" + {r}")
|
|
152
|
+
if skipped:
|
|
153
|
+
print(f"skipped {len(skipped)} existing file(s) (use --force to overwrite):")
|
|
154
|
+
for r in skipped:
|
|
155
|
+
print(f" = {r}")
|
|
156
|
+
if not args.dry_run and not args.template:
|
|
157
|
+
cmds = ", ".join(f"/{c}" for c in COMMANDS)
|
|
158
|
+
print(f"\nCommands available in this project: {cmds}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|