@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/tools/test_kit.py
CHANGED
|
@@ -1,363 +1,392 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
test_kit.py — Comprehensive edge-case test suite for agents-maker.
|
|
4
|
-
|
|
5
|
-
Run from the repo root:
|
|
6
|
-
python tools/test_kit.py
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
sys.stdout.reconfigure(encoding="utf-8")
|
|
16
|
-
sys.stderr.reconfigure(encoding="utf-8")
|
|
17
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
18
|
-
PY = sys.executable
|
|
19
|
-
|
|
20
|
-
PASS_COUNT = 0
|
|
21
|
-
FAIL_COUNT = 0
|
|
22
|
-
RESULTS = []
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def run(cmd: list[str], cwd: Path = ROOT, input_text: str | None = None) -> tuple[int, str, str]:
|
|
26
|
-
result = subprocess.run(
|
|
27
|
-
cmd, cwd=str(cwd), capture_output=True, text=True,
|
|
28
|
-
input=input_text, encoding="utf-8", errors="replace"
|
|
29
|
-
)
|
|
30
|
-
return result.returncode, result.stdout, result.stderr
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def check(n: int, desc: str, passed: bool, reason: str = "") -> None:
|
|
34
|
-
global PASS_COUNT, FAIL_COUNT
|
|
35
|
-
if passed:
|
|
36
|
-
PASS_COUNT += 1
|
|
37
|
-
tag = f"PASS [{n:02d}]"
|
|
38
|
-
else:
|
|
39
|
-
FAIL_COUNT += 1
|
|
40
|
-
tag = f"FAIL [{n:02d}]"
|
|
41
|
-
msg = f"{tag} {desc}"
|
|
42
|
-
if not passed and reason:
|
|
43
|
-
msg += f"\n Reason: {reason}"
|
|
44
|
-
print(msg)
|
|
45
|
-
RESULTS.append((n, passed, desc, reason))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def section(title: str) -> None:
|
|
49
|
-
print(f"\n{'-' * 60}")
|
|
50
|
-
print(f" {title}")
|
|
51
|
-
print('-' * 60)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# -------------------------------------------------------------
|
|
55
|
-
# A. generate_prompt.py — normal cases
|
|
56
|
-
# -------------------------------------------------------------
|
|
57
|
-
section("A. generate_prompt.py — normal cases")
|
|
58
|
-
|
|
59
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting to the auth service"])
|
|
60
|
-
check(1, "Basic run exits 0", rc == 0, err[:200] if rc != 0 else "")
|
|
61
|
-
check(2, "Output contains ## Project Context", "## Project Context" in out, "")
|
|
62
|
-
check(3, "Software domain auto-detected", "software" in out.lower(), out[:300])
|
|
63
|
-
|
|
64
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--phase", "implementation"])
|
|
65
|
-
check(4, "--phase implementation -> output contains 'implementation'", "implementation" in out, "")
|
|
66
|
-
|
|
67
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--phase", "review"])
|
|
68
|
-
check(5, "--phase review -> output contains 'review'", "review" in out, "")
|
|
69
|
-
|
|
70
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--full"])
|
|
71
|
-
check(6, "--full flag -> output contains orchestrator content", "Orchestrator" in out or "orchestrator" in out, "")
|
|
72
|
-
|
|
73
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--compress"])
|
|
74
|
-
check(7, "--compress flag -> output contains ## Output Policy", "## Output Policy" in out, out[-300:])
|
|
75
|
-
|
|
76
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "[domain: content] write a blog post about GraphQL"])
|
|
77
|
-
check(8, "[domain: content] prefix -> domain is content", "content" in out.lower(), "")
|
|
78
|
-
|
|
79
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "[domain: ops_process] write a runbook for failover"])
|
|
80
|
-
check(9, "[domain: ops_process] prefix -> execution_agent or ops_process in output", "ops_process" in out.lower() or "execution_agent" in out.lower(), out[:300])
|
|
81
|
-
|
|
82
|
-
# -------------------------------------------------------------
|
|
83
|
-
# B. generate_prompt.py — edge cases / error handling
|
|
84
|
-
# -------------------------------------------------------------
|
|
85
|
-
section("B. generate_prompt.py — edge cases / error handling")
|
|
86
|
-
|
|
87
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", ""])
|
|
88
|
-
check(10, "Empty problem -> non-zero exit", rc != 0, f"exit={rc}")
|
|
89
|
-
check(11, "Empty problem -> stderr contains 'empty'", "empty" in err.lower(), err[:200])
|
|
90
|
-
|
|
91
|
-
long_input = "x" * 5001
|
|
92
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", long_input])
|
|
93
|
-
check(12, "Problem > 5000 chars -> non-zero exit", rc != 0, f"exit={rc}")
|
|
94
|
-
check(13, "Problem > 5000 chars -> stderr contains 'too long'", "too long" in err.lower(), err[:200])
|
|
95
|
-
|
|
96
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "task", "--phase", "invalid_phase"])
|
|
97
|
-
check(14, "Invalid --phase value -> non-zero exit", rc != 0, f"exit={rc}")
|
|
98
|
-
|
|
99
|
-
valid_phases = ["task_framing", "requirements", "design", "solution_design",
|
|
100
|
-
"implementation", "implement", "review", "review_refinement", "handoff", "framing"]
|
|
101
|
-
all_phases_ok = True
|
|
102
|
-
failed_phase = ""
|
|
103
|
-
for ph in valid_phases:
|
|
104
|
-
rc2, _, err2 = run([PY, "tools/generate_prompt.py", "some task", "--phase", ph])
|
|
105
|
-
if rc2 != 0:
|
|
106
|
-
all_phases_ok = False
|
|
107
|
-
failed_phase = f"phase={ph} exit={rc2} err={err2[:100]}"
|
|
108
|
-
break
|
|
109
|
-
check(15, "All valid --phase values exit 0", all_phases_ok, failed_phase)
|
|
110
|
-
|
|
111
|
-
# -------------------------------------------------------------
|
|
112
|
-
# C. Domain detection — all 8 domains
|
|
113
|
-
# -------------------------------------------------------------
|
|
114
|
-
section("C. Domain detection — all 8 domains")
|
|
115
|
-
|
|
116
|
-
domain_tests = [
|
|
117
|
-
(16, "software", "Refactor the UserRepository class and add unit tests for the API endpoints"),
|
|
118
|
-
(17, "content", "Write a technical blog post about our REST to GraphQL migration for developers"),
|
|
119
|
-
(18, "research", "Write a literature review on transformer model architectures for NLP"),
|
|
120
|
-
(19, "data_analytics", "Build a sales funnel dashboard with conversion metrics and pipeline analytics"),
|
|
121
|
-
(20, "product_design", "Design a mobile onboarding flow for our e-commerce app with persona mapping"),
|
|
122
|
-
(21, "marketing", "Create a go-to-market strategy and campaign brief for our SaaS product launch"),
|
|
123
|
-
(22, "ops_process", "Write a runbook SOP for our database failover and disaster recovery procedure"),
|
|
124
|
-
(23, "general", "I need help with something"),
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
for n, expected_domain, task in domain_tests:
|
|
128
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", task])
|
|
129
|
-
combined = out.lower() + err.lower()
|
|
130
|
-
if expected_domain == "general":
|
|
131
|
-
# Low-confidence tasks fall back to project domain when project.yaml has primary_domain set.
|
|
132
|
-
# Accept either "general" OR "from-project" (scoring fell below threshold, used project default).
|
|
133
|
-
found = "general" in combined or "from-project" in combined
|
|
134
|
-
else:
|
|
135
|
-
found = expected_domain in combined
|
|
136
|
-
check(n, f"Domain detection -> {expected_domain}", found, f"task='{task[:50]}...' output snippet: {out[:200]}")
|
|
137
|
-
|
|
138
|
-
# -------------------------------------------------------------
|
|
139
|
-
# D. validate_kit.py — all 12 checks pass
|
|
140
|
-
# -------------------------------------------------------------
|
|
141
|
-
section("D. validate_kit.py — all 12 checks pass normally")
|
|
142
|
-
|
|
143
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
144
|
-
check(24, "validate_kit.py exits 0", rc == 0, err[:200])
|
|
145
|
-
check(25, "ALL 12 checks PASSED in output", "ALL 12 checks PASSED" in out, out[-300:])
|
|
146
|
-
|
|
147
|
-
# -------------------------------------------------------------
|
|
148
|
-
# E. validate_kit.py — failure detection (temporarily corrupt files)
|
|
149
|
-
# -------------------------------------------------------------
|
|
150
|
-
section("E. validate_kit.py — failure detection")
|
|
151
|
-
|
|
152
|
-
orch_path = ROOT / "agents" / "orchestrator.md"
|
|
153
|
-
orch_backup = orch_path.read_text(encoding="utf-8")
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
orch_path.rename(ROOT / "agents" / "orchestrator.md.bak")
|
|
157
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
158
|
-
check(26, "Missing agent file -> FAIL in output", "FAIL" in out, out[-300:])
|
|
159
|
-
finally:
|
|
160
|
-
bak = ROOT / "agents" / "orchestrator.md.bak"
|
|
161
|
-
if bak.exists():
|
|
162
|
-
bak.rename(orch_path)
|
|
163
|
-
|
|
164
|
-
skill_path = ROOT / "skills" / "analyze_repo.md"
|
|
165
|
-
skill_backup = skill_path.read_text(encoding="utf-8")
|
|
166
|
-
try:
|
|
167
|
-
skill_path.write_text("short", encoding="utf-8")
|
|
168
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
169
|
-
check(27, "Stub skill file (< 50 chars) -> FAIL in output", "FAIL" in out, out[-300:])
|
|
170
|
-
finally:
|
|
171
|
-
skill_path.write_text(skill_backup, encoding="utf-8")
|
|
172
|
-
|
|
173
|
-
try:
|
|
174
|
-
skill_path.write_text("# Skill: test\n\nNo input or output section here.\n\nToken budget: low\n", encoding="utf-8")
|
|
175
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
176
|
-
check(28, "Skill missing input/output headings -> FAIL in output", "FAIL" in out, out[-300:])
|
|
177
|
-
finally:
|
|
178
|
-
skill_path.write_text(skill_backup, encoding="utf-8")
|
|
179
|
-
|
|
180
|
-
agent_path = ROOT / "agents" / "orchestrator.md"
|
|
181
|
-
agent_backup = agent_path.read_text(encoding="utf-8")
|
|
182
|
-
try:
|
|
183
|
-
agent_path.write_text("# Orchestrator\n\nSome content without required sections.\n" * 5, encoding="utf-8")
|
|
184
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
185
|
-
check(29, "Agent missing ## Role/Goals/Context -> FAIL in output", "FAIL" in out, out[-300:])
|
|
186
|
-
finally:
|
|
187
|
-
agent_path.write_text(agent_backup, encoding="utf-8")
|
|
188
|
-
|
|
189
|
-
# Confirm restore worked
|
|
190
|
-
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
191
|
-
check(30, "All files restored -> validator passes again", "ALL 12 checks PASSED" in out, out[-200:])
|
|
192
|
-
|
|
193
|
-
# -------------------------------------------------------------
|
|
194
|
-
# F. Compressor / PolicyLoader — direct import tests
|
|
195
|
-
# -------------------------------------------------------------
|
|
196
|
-
section("F. Compressor / PolicyLoader — direct import tests")
|
|
197
|
-
|
|
198
|
-
sys.path.insert(0, str(ROOT))
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
from token_optimization.compressor import (
|
|
202
|
-
CompressionReport,
|
|
203
|
-
Compressor,
|
|
204
|
-
ContextBlock,
|
|
205
|
-
PolicyLoader,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
loader = PolicyLoader(ROOT / "config" / "token_policies.yaml")
|
|
209
|
-
loader.load()
|
|
210
|
-
check(31, "PolicyLoader imports and loads without error", True, "")
|
|
211
|
-
|
|
212
|
-
for wf in ["feature_implementation", "code_review", "feature_design"]:
|
|
213
|
-
policy = loader.get_workflow_policy(wf)
|
|
214
|
-
ok_wf = policy.max_input_tokens > 0 and bool(policy.output_style)
|
|
215
|
-
check({"feature_implementation": 32, "code_review": 33, "feature_design": 34}[wf],
|
|
216
|
-
f"get_workflow_policy('{wf}') returns valid policy",
|
|
217
|
-
ok_wf, f"max_input_tokens={policy.max_input_tokens} output_style={policy.output_style}")
|
|
218
|
-
|
|
219
|
-
unknown_policy = loader.get_workflow_policy("completely_unknown_workflow_xyz")
|
|
220
|
-
check(35, "Unknown workflow -> fallback policy, no crash",
|
|
221
|
-
unknown_policy is not None and unknown_policy.max_input_tokens >= 0, "")
|
|
222
|
-
|
|
223
|
-
policy = loader.get_workflow_policy("feature_implementation")
|
|
224
|
-
compressor = Compressor(policy)
|
|
225
|
-
empty_block = ContextBlock()
|
|
226
|
-
compressed, report = compressor.compress(empty_block)
|
|
227
|
-
check(36, "compress(empty ContextBlock) returns without crash",
|
|
228
|
-
isinstance(compressed, str) and isinstance(report, CompressionReport), "")
|
|
229
|
-
|
|
230
|
-
block_with_state = ContextBlock(
|
|
231
|
-
project_state="Name: test-project\nStack: Python, FastAPI\nDomain: software",
|
|
232
|
-
active_query="add rate limiting to the auth service",
|
|
233
|
-
conversation_state="User asked about rate limiting. We discussed Redis sliding window.",
|
|
234
|
-
)
|
|
235
|
-
compressed2, report2 = compressor.compress(block_with_state)
|
|
236
|
-
check(37, "compress(ContextBlock with content) returns non-empty string",
|
|
237
|
-
isinstance(compressed2, str) and len(compressed2) > 0, f"len={len(compressed2)}")
|
|
238
|
-
|
|
239
|
-
loader_bad = PolicyLoader("/nonexistent/path/token_policies.yaml")
|
|
240
|
-
try:
|
|
241
|
-
loader_bad.load()
|
|
242
|
-
check(38, "PolicyLoader with bad path -> graceful fallback, no exception", True, "")
|
|
243
|
-
except Exception as e:
|
|
244
|
-
check(38, "PolicyLoader with bad path -> graceful fallback, no exception", False, str(e))
|
|
245
|
-
|
|
246
|
-
except ImportError as e:
|
|
247
|
-
for n in range(31, 39):
|
|
248
|
-
check(n, "Compressor import test", False, str(e))
|
|
249
|
-
|
|
250
|
-
# -------------------------------------------------------------
|
|
251
|
-
# G. Context loaders — smoke tests
|
|
252
|
-
# -------------------------------------------------------------
|
|
253
|
-
section("G. Context loaders — smoke tests")
|
|
254
|
-
|
|
255
|
-
rc, out, err = run([PY, "context_loaders/project_summary.py", "--path", str(ROOT)])
|
|
256
|
-
check(39, "project_summary.py exits 0", rc == 0, err[:200])
|
|
257
|
-
check(40, "project_summary.py output contains 'Stack' or 'Language'",
|
|
258
|
-
"stack" in out.lower() or "language" in out.lower(), out[:300])
|
|
259
|
-
|
|
260
|
-
rc, out, err = run([PY, "context_loaders/repo_tree.py", "--path", str(ROOT)])
|
|
261
|
-
check(41, "repo_tree.py exits 0", rc == 0, err[:200])
|
|
262
|
-
check(42, "repo_tree.py output contains directory structure",
|
|
263
|
-
"agents" in out.lower() or "skills" in out.lower(), out[:300])
|
|
264
|
-
|
|
265
|
-
rc, out, err = run([PY, "context_loaders/file_chunker.py", "--path", str(ROOT), "--files", "README.md"])
|
|
266
|
-
check(43, "file_chunker.py exits 0", rc == 0, err[:200])
|
|
267
|
-
check(44, "file_chunker.py output contains README content",
|
|
268
|
-
"agents-maker" in out.lower() or "readme" in out.lower(), out[:300])
|
|
269
|
-
|
|
270
|
-
# -------------------------------------------------------------
|
|
271
|
-
# H. init_project.py — smoke tests
|
|
272
|
-
# -------------------------------------------------------------
|
|
273
|
-
section("H. init_project.py — smoke tests")
|
|
274
|
-
|
|
275
|
-
rc, out, err = run([PY, "tools/init_project.py", "--path", str(ROOT), "--update"])
|
|
276
|
-
check(45, "init_project.py --update exits 0", rc == 0, err[:300])
|
|
277
|
-
check(46, "system_prompt.md still exists after --update", (ROOT / "system_prompt.md").exists(), "")
|
|
278
|
-
|
|
279
|
-
# -------------------------------------------------------------
|
|
280
|
-
# I. generate_prompt.py — missing project.yaml
|
|
281
|
-
# -------------------------------------------------------------
|
|
282
|
-
section("I. generate_prompt.py — missing project.yaml")
|
|
283
|
-
|
|
284
|
-
proj_yaml = ROOT / "config" / "project.yaml"
|
|
285
|
-
proj_backup = None
|
|
286
|
-
renamed = False
|
|
287
|
-
|
|
288
|
-
if proj_yaml.exists():
|
|
289
|
-
proj_backup = proj_yaml.read_text(encoding="utf-8")
|
|
290
|
-
proj_yaml.rename(ROOT / "config" / "project.yaml.bak")
|
|
291
|
-
renamed = True
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting"])
|
|
295
|
-
check(47, "Missing project.yaml -> tool still exits 0", rc == 0, err[:200])
|
|
296
|
-
check(48, "Missing project.yaml -> [WARN] printed to stderr", "[WARN]" in err, err[:200])
|
|
297
|
-
finally:
|
|
298
|
-
bak = ROOT / "config" / "project.yaml.bak"
|
|
299
|
-
if bak.exists():
|
|
300
|
-
bak.rename(proj_yaml)
|
|
301
|
-
elif proj_backup and not proj_yaml.exists():
|
|
302
|
-
proj_yaml.write_text(proj_backup, encoding="utf-8")
|
|
303
|
-
|
|
304
|
-
# -------------------------------------------------------------
|
|
305
|
-
# J. system_prompt.md integrity
|
|
306
|
-
# -------------------------------------------------------------
|
|
307
|
-
section("J. system_prompt.md integrity")
|
|
308
|
-
|
|
309
|
-
sp = ROOT / "system_prompt.md"
|
|
310
|
-
check(49, "system_prompt.md exists", sp.exists(), "")
|
|
311
|
-
if sp.exists():
|
|
312
|
-
sp_text = sp.read_text(encoding="utf-8")
|
|
313
|
-
check(50, "system_prompt.md > 10,000 chars", len(sp_text) > 10000, f"actual: {len(sp_text)}")
|
|
314
|
-
check(51, "system_prompt.md contains orchestrator content (## Role or ## Goals)",
|
|
315
|
-
"## role" in sp_text.lower() or "## goals" in sp_text.lower(), "")
|
|
316
|
-
check(52, "system_prompt.md contains version header",
|
|
317
|
-
"agents-maker system_prompt.md" in sp_text, sp_text[:200])
|
|
318
|
-
check(53, "system_prompt.md contains [Companion] instruction",
|
|
319
|
-
"[Companion]" in sp_text, sp_text[:500])
|
|
320
|
-
else:
|
|
321
|
-
for n in range(50, 54):
|
|
322
|
-
check(n, "system_prompt.md content check", False, "file missing")
|
|
323
|
-
|
|
324
|
-
# -------------------------------------------------------------
|
|
325
|
-
# K. generate_prompt.py — --compress with all phases
|
|
326
|
-
# -------------------------------------------------------------
|
|
327
|
-
section("K. --compress with all phases")
|
|
328
|
-
|
|
329
|
-
compress_phases = ["task_framing", "implementation", "review_refinement", "handoff", "solution_design"]
|
|
330
|
-
for i, ph in enumerate(compress_phases):
|
|
331
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "some task", "--phase", ph, "--compress"])
|
|
332
|
-
n = 54 + i
|
|
333
|
-
check(n, f"--compress --phase {ph} -> exits 0 + Output Policy in output",
|
|
334
|
-
rc == 0 and "## Output Policy" in out,
|
|
335
|
-
f"exit={rc} stderr={err[:100]}")
|
|
336
|
-
|
|
337
|
-
# -------------------------------------------------------------
|
|
338
|
-
# L. --full + --compress combined
|
|
339
|
-
# -------------------------------------------------------------
|
|
340
|
-
section("L. --full + --compress combined")
|
|
341
|
-
|
|
342
|
-
rc, out, err = run([PY, "tools/generate_prompt.py", "add feature", "--full", "--compress"])
|
|
343
|
-
check(59, "--full --compress combined -> exits 0", rc == 0, err[:200])
|
|
344
|
-
check(60, "--full --compress -> contains both system prompt and Output Policy",
|
|
345
|
-
("Orchestrator" in out or "orchestrator" in out) and "## Output Policy" in out, "")
|
|
346
|
-
|
|
347
|
-
# -------------------------------------------------------------
|
|
348
|
-
#
|
|
349
|
-
# -------------------------------------------------------------
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
test_kit.py — Comprehensive edge-case test suite for agents-maker.
|
|
4
|
+
|
|
5
|
+
Run from the repo root:
|
|
6
|
+
python tools/test_kit.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
16
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
17
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
18
|
+
PY = sys.executable
|
|
19
|
+
|
|
20
|
+
PASS_COUNT = 0
|
|
21
|
+
FAIL_COUNT = 0
|
|
22
|
+
RESULTS = []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run(cmd: list[str], cwd: Path = ROOT, input_text: str | None = None) -> tuple[int, str, str]:
|
|
26
|
+
result = subprocess.run(
|
|
27
|
+
cmd, cwd=str(cwd), capture_output=True, text=True,
|
|
28
|
+
input=input_text, encoding="utf-8", errors="replace"
|
|
29
|
+
)
|
|
30
|
+
return result.returncode, result.stdout, result.stderr
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check(n: int, desc: str, passed: bool, reason: str = "") -> None:
|
|
34
|
+
global PASS_COUNT, FAIL_COUNT
|
|
35
|
+
if passed:
|
|
36
|
+
PASS_COUNT += 1
|
|
37
|
+
tag = f"PASS [{n:02d}]"
|
|
38
|
+
else:
|
|
39
|
+
FAIL_COUNT += 1
|
|
40
|
+
tag = f"FAIL [{n:02d}]"
|
|
41
|
+
msg = f"{tag} {desc}"
|
|
42
|
+
if not passed and reason:
|
|
43
|
+
msg += f"\n Reason: {reason}"
|
|
44
|
+
print(msg)
|
|
45
|
+
RESULTS.append((n, passed, desc, reason))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def section(title: str) -> None:
|
|
49
|
+
print(f"\n{'-' * 60}")
|
|
50
|
+
print(f" {title}")
|
|
51
|
+
print('-' * 60)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# -------------------------------------------------------------
|
|
55
|
+
# A. generate_prompt.py — normal cases
|
|
56
|
+
# -------------------------------------------------------------
|
|
57
|
+
section("A. generate_prompt.py — normal cases")
|
|
58
|
+
|
|
59
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting to the auth service"])
|
|
60
|
+
check(1, "Basic run exits 0", rc == 0, err[:200] if rc != 0 else "")
|
|
61
|
+
check(2, "Output contains ## Project Context", "## Project Context" in out, "")
|
|
62
|
+
check(3, "Software domain auto-detected", "software" in out.lower(), out[:300])
|
|
63
|
+
|
|
64
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--phase", "implementation"])
|
|
65
|
+
check(4, "--phase implementation -> output contains 'implementation'", "implementation" in out, "")
|
|
66
|
+
|
|
67
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--phase", "review"])
|
|
68
|
+
check(5, "--phase review -> output contains 'review'", "review" in out, "")
|
|
69
|
+
|
|
70
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--full"])
|
|
71
|
+
check(6, "--full flag -> output contains orchestrator content", "Orchestrator" in out or "orchestrator" in out, "")
|
|
72
|
+
|
|
73
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting", "--compress"])
|
|
74
|
+
check(7, "--compress flag -> output contains ## Output Policy", "## Output Policy" in out, out[-300:])
|
|
75
|
+
|
|
76
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "[domain: content] write a blog post about GraphQL"])
|
|
77
|
+
check(8, "[domain: content] prefix -> domain is content", "content" in out.lower(), "")
|
|
78
|
+
|
|
79
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "[domain: ops_process] write a runbook for failover"])
|
|
80
|
+
check(9, "[domain: ops_process] prefix -> execution_agent or ops_process in output", "ops_process" in out.lower() or "execution_agent" in out.lower(), out[:300])
|
|
81
|
+
|
|
82
|
+
# -------------------------------------------------------------
|
|
83
|
+
# B. generate_prompt.py — edge cases / error handling
|
|
84
|
+
# -------------------------------------------------------------
|
|
85
|
+
section("B. generate_prompt.py — edge cases / error handling")
|
|
86
|
+
|
|
87
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", ""])
|
|
88
|
+
check(10, "Empty problem -> non-zero exit", rc != 0, f"exit={rc}")
|
|
89
|
+
check(11, "Empty problem -> stderr contains 'empty'", "empty" in err.lower(), err[:200])
|
|
90
|
+
|
|
91
|
+
long_input = "x" * 5001
|
|
92
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", long_input])
|
|
93
|
+
check(12, "Problem > 5000 chars -> non-zero exit", rc != 0, f"exit={rc}")
|
|
94
|
+
check(13, "Problem > 5000 chars -> stderr contains 'too long'", "too long" in err.lower(), err[:200])
|
|
95
|
+
|
|
96
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "task", "--phase", "invalid_phase"])
|
|
97
|
+
check(14, "Invalid --phase value -> non-zero exit", rc != 0, f"exit={rc}")
|
|
98
|
+
|
|
99
|
+
valid_phases = ["task_framing", "requirements", "design", "solution_design",
|
|
100
|
+
"implementation", "implement", "review", "review_refinement", "handoff", "framing"]
|
|
101
|
+
all_phases_ok = True
|
|
102
|
+
failed_phase = ""
|
|
103
|
+
for ph in valid_phases:
|
|
104
|
+
rc2, _, err2 = run([PY, "tools/generate_prompt.py", "some task", "--phase", ph])
|
|
105
|
+
if rc2 != 0:
|
|
106
|
+
all_phases_ok = False
|
|
107
|
+
failed_phase = f"phase={ph} exit={rc2} err={err2[:100]}"
|
|
108
|
+
break
|
|
109
|
+
check(15, "All valid --phase values exit 0", all_phases_ok, failed_phase)
|
|
110
|
+
|
|
111
|
+
# -------------------------------------------------------------
|
|
112
|
+
# C. Domain detection — all 8 domains
|
|
113
|
+
# -------------------------------------------------------------
|
|
114
|
+
section("C. Domain detection — all 8 domains")
|
|
115
|
+
|
|
116
|
+
domain_tests = [
|
|
117
|
+
(16, "software", "Refactor the UserRepository class and add unit tests for the API endpoints"),
|
|
118
|
+
(17, "content", "Write a technical blog post about our REST to GraphQL migration for developers"),
|
|
119
|
+
(18, "research", "Write a literature review on transformer model architectures for NLP"),
|
|
120
|
+
(19, "data_analytics", "Build a sales funnel dashboard with conversion metrics and pipeline analytics"),
|
|
121
|
+
(20, "product_design", "Design a mobile onboarding flow for our e-commerce app with persona mapping"),
|
|
122
|
+
(21, "marketing", "Create a go-to-market strategy and campaign brief for our SaaS product launch"),
|
|
123
|
+
(22, "ops_process", "Write a runbook SOP for our database failover and disaster recovery procedure"),
|
|
124
|
+
(23, "general", "I need help with something"),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
for n, expected_domain, task in domain_tests:
|
|
128
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", task])
|
|
129
|
+
combined = out.lower() + err.lower()
|
|
130
|
+
if expected_domain == "general":
|
|
131
|
+
# Low-confidence tasks fall back to project domain when project.yaml has primary_domain set.
|
|
132
|
+
# Accept either "general" OR "from-project" (scoring fell below threshold, used project default).
|
|
133
|
+
found = "general" in combined or "from-project" in combined
|
|
134
|
+
else:
|
|
135
|
+
found = expected_domain in combined
|
|
136
|
+
check(n, f"Domain detection -> {expected_domain}", found, f"task='{task[:50]}...' output snippet: {out[:200]}")
|
|
137
|
+
|
|
138
|
+
# -------------------------------------------------------------
|
|
139
|
+
# D. validate_kit.py — all 12 checks pass
|
|
140
|
+
# -------------------------------------------------------------
|
|
141
|
+
section("D. validate_kit.py — all 12 checks pass normally")
|
|
142
|
+
|
|
143
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
144
|
+
check(24, "validate_kit.py exits 0", rc == 0, err[:200])
|
|
145
|
+
check(25, "ALL 12 checks PASSED in output", "ALL 12 checks PASSED" in out, out[-300:])
|
|
146
|
+
|
|
147
|
+
# -------------------------------------------------------------
|
|
148
|
+
# E. validate_kit.py — failure detection (temporarily corrupt files)
|
|
149
|
+
# -------------------------------------------------------------
|
|
150
|
+
section("E. validate_kit.py — failure detection")
|
|
151
|
+
|
|
152
|
+
orch_path = ROOT / "agents" / "orchestrator.md"
|
|
153
|
+
orch_backup = orch_path.read_text(encoding="utf-8")
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
orch_path.rename(ROOT / "agents" / "orchestrator.md.bak")
|
|
157
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
158
|
+
check(26, "Missing agent file -> FAIL in output", "FAIL" in out, out[-300:])
|
|
159
|
+
finally:
|
|
160
|
+
bak = ROOT / "agents" / "orchestrator.md.bak"
|
|
161
|
+
if bak.exists():
|
|
162
|
+
bak.rename(orch_path)
|
|
163
|
+
|
|
164
|
+
skill_path = ROOT / "skills" / "analyze_repo.md"
|
|
165
|
+
skill_backup = skill_path.read_text(encoding="utf-8")
|
|
166
|
+
try:
|
|
167
|
+
skill_path.write_text("short", encoding="utf-8")
|
|
168
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
169
|
+
check(27, "Stub skill file (< 50 chars) -> FAIL in output", "FAIL" in out, out[-300:])
|
|
170
|
+
finally:
|
|
171
|
+
skill_path.write_text(skill_backup, encoding="utf-8")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
skill_path.write_text("# Skill: test\n\nNo input or output section here.\n\nToken budget: low\n", encoding="utf-8")
|
|
175
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
176
|
+
check(28, "Skill missing input/output headings -> FAIL in output", "FAIL" in out, out[-300:])
|
|
177
|
+
finally:
|
|
178
|
+
skill_path.write_text(skill_backup, encoding="utf-8")
|
|
179
|
+
|
|
180
|
+
agent_path = ROOT / "agents" / "orchestrator.md"
|
|
181
|
+
agent_backup = agent_path.read_text(encoding="utf-8")
|
|
182
|
+
try:
|
|
183
|
+
agent_path.write_text("# Orchestrator\n\nSome content without required sections.\n" * 5, encoding="utf-8")
|
|
184
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
185
|
+
check(29, "Agent missing ## Role/Goals/Context -> FAIL in output", "FAIL" in out, out[-300:])
|
|
186
|
+
finally:
|
|
187
|
+
agent_path.write_text(agent_backup, encoding="utf-8")
|
|
188
|
+
|
|
189
|
+
# Confirm restore worked
|
|
190
|
+
rc, out, err = run([PY, "tools/validate_kit.py"])
|
|
191
|
+
check(30, "All files restored -> validator passes again", "ALL 12 checks PASSED" in out, out[-200:])
|
|
192
|
+
|
|
193
|
+
# -------------------------------------------------------------
|
|
194
|
+
# F. Compressor / PolicyLoader — direct import tests
|
|
195
|
+
# -------------------------------------------------------------
|
|
196
|
+
section("F. Compressor / PolicyLoader — direct import tests")
|
|
197
|
+
|
|
198
|
+
sys.path.insert(0, str(ROOT))
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
from token_optimization.compressor import (
|
|
202
|
+
CompressionReport,
|
|
203
|
+
Compressor,
|
|
204
|
+
ContextBlock,
|
|
205
|
+
PolicyLoader,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
loader = PolicyLoader(ROOT / "config" / "token_policies.yaml")
|
|
209
|
+
loader.load()
|
|
210
|
+
check(31, "PolicyLoader imports and loads without error", True, "")
|
|
211
|
+
|
|
212
|
+
for wf in ["feature_implementation", "code_review", "feature_design"]:
|
|
213
|
+
policy = loader.get_workflow_policy(wf)
|
|
214
|
+
ok_wf = policy.max_input_tokens > 0 and bool(policy.output_style)
|
|
215
|
+
check({"feature_implementation": 32, "code_review": 33, "feature_design": 34}[wf],
|
|
216
|
+
f"get_workflow_policy('{wf}') returns valid policy",
|
|
217
|
+
ok_wf, f"max_input_tokens={policy.max_input_tokens} output_style={policy.output_style}")
|
|
218
|
+
|
|
219
|
+
unknown_policy = loader.get_workflow_policy("completely_unknown_workflow_xyz")
|
|
220
|
+
check(35, "Unknown workflow -> fallback policy, no crash",
|
|
221
|
+
unknown_policy is not None and unknown_policy.max_input_tokens >= 0, "")
|
|
222
|
+
|
|
223
|
+
policy = loader.get_workflow_policy("feature_implementation")
|
|
224
|
+
compressor = Compressor(policy)
|
|
225
|
+
empty_block = ContextBlock()
|
|
226
|
+
compressed, report = compressor.compress(empty_block)
|
|
227
|
+
check(36, "compress(empty ContextBlock) returns without crash",
|
|
228
|
+
isinstance(compressed, str) and isinstance(report, CompressionReport), "")
|
|
229
|
+
|
|
230
|
+
block_with_state = ContextBlock(
|
|
231
|
+
project_state="Name: test-project\nStack: Python, FastAPI\nDomain: software",
|
|
232
|
+
active_query="add rate limiting to the auth service",
|
|
233
|
+
conversation_state="User asked about rate limiting. We discussed Redis sliding window.",
|
|
234
|
+
)
|
|
235
|
+
compressed2, report2 = compressor.compress(block_with_state)
|
|
236
|
+
check(37, "compress(ContextBlock with content) returns non-empty string",
|
|
237
|
+
isinstance(compressed2, str) and len(compressed2) > 0, f"len={len(compressed2)}")
|
|
238
|
+
|
|
239
|
+
loader_bad = PolicyLoader("/nonexistent/path/token_policies.yaml")
|
|
240
|
+
try:
|
|
241
|
+
loader_bad.load()
|
|
242
|
+
check(38, "PolicyLoader with bad path -> graceful fallback, no exception", True, "")
|
|
243
|
+
except Exception as e:
|
|
244
|
+
check(38, "PolicyLoader with bad path -> graceful fallback, no exception", False, str(e))
|
|
245
|
+
|
|
246
|
+
except ImportError as e:
|
|
247
|
+
for n in range(31, 39):
|
|
248
|
+
check(n, "Compressor import test", False, str(e))
|
|
249
|
+
|
|
250
|
+
# -------------------------------------------------------------
|
|
251
|
+
# G. Context loaders — smoke tests
|
|
252
|
+
# -------------------------------------------------------------
|
|
253
|
+
section("G. Context loaders — smoke tests")
|
|
254
|
+
|
|
255
|
+
rc, out, err = run([PY, "context_loaders/project_summary.py", "--path", str(ROOT)])
|
|
256
|
+
check(39, "project_summary.py exits 0", rc == 0, err[:200])
|
|
257
|
+
check(40, "project_summary.py output contains 'Stack' or 'Language'",
|
|
258
|
+
"stack" in out.lower() or "language" in out.lower(), out[:300])
|
|
259
|
+
|
|
260
|
+
rc, out, err = run([PY, "context_loaders/repo_tree.py", "--path", str(ROOT)])
|
|
261
|
+
check(41, "repo_tree.py exits 0", rc == 0, err[:200])
|
|
262
|
+
check(42, "repo_tree.py output contains directory structure",
|
|
263
|
+
"agents" in out.lower() or "skills" in out.lower(), out[:300])
|
|
264
|
+
|
|
265
|
+
rc, out, err = run([PY, "context_loaders/file_chunker.py", "--path", str(ROOT), "--files", "README.md"])
|
|
266
|
+
check(43, "file_chunker.py exits 0", rc == 0, err[:200])
|
|
267
|
+
check(44, "file_chunker.py output contains README content",
|
|
268
|
+
"agents-maker" in out.lower() or "readme" in out.lower(), out[:300])
|
|
269
|
+
|
|
270
|
+
# -------------------------------------------------------------
|
|
271
|
+
# H. init_project.py — smoke tests
|
|
272
|
+
# -------------------------------------------------------------
|
|
273
|
+
section("H. init_project.py — smoke tests")
|
|
274
|
+
|
|
275
|
+
rc, out, err = run([PY, "tools/init_project.py", "--path", str(ROOT), "--update"])
|
|
276
|
+
check(45, "init_project.py --update exits 0", rc == 0, err[:300])
|
|
277
|
+
check(46, "system_prompt.md still exists after --update", (ROOT / "system_prompt.md").exists(), "")
|
|
278
|
+
|
|
279
|
+
# -------------------------------------------------------------
|
|
280
|
+
# I. generate_prompt.py — missing project.yaml
|
|
281
|
+
# -------------------------------------------------------------
|
|
282
|
+
section("I. generate_prompt.py — missing project.yaml")
|
|
283
|
+
|
|
284
|
+
proj_yaml = ROOT / "config" / "project.yaml"
|
|
285
|
+
proj_backup = None
|
|
286
|
+
renamed = False
|
|
287
|
+
|
|
288
|
+
if proj_yaml.exists():
|
|
289
|
+
proj_backup = proj_yaml.read_text(encoding="utf-8")
|
|
290
|
+
proj_yaml.rename(ROOT / "config" / "project.yaml.bak")
|
|
291
|
+
renamed = True
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add rate limiting"])
|
|
295
|
+
check(47, "Missing project.yaml -> tool still exits 0", rc == 0, err[:200])
|
|
296
|
+
check(48, "Missing project.yaml -> [WARN] printed to stderr", "[WARN]" in err, err[:200])
|
|
297
|
+
finally:
|
|
298
|
+
bak = ROOT / "config" / "project.yaml.bak"
|
|
299
|
+
if bak.exists():
|
|
300
|
+
bak.rename(proj_yaml)
|
|
301
|
+
elif proj_backup and not proj_yaml.exists():
|
|
302
|
+
proj_yaml.write_text(proj_backup, encoding="utf-8")
|
|
303
|
+
|
|
304
|
+
# -------------------------------------------------------------
|
|
305
|
+
# J. system_prompt.md integrity
|
|
306
|
+
# -------------------------------------------------------------
|
|
307
|
+
section("J. system_prompt.md integrity")
|
|
308
|
+
|
|
309
|
+
sp = ROOT / "system_prompt.md"
|
|
310
|
+
check(49, "system_prompt.md exists", sp.exists(), "")
|
|
311
|
+
if sp.exists():
|
|
312
|
+
sp_text = sp.read_text(encoding="utf-8")
|
|
313
|
+
check(50, "system_prompt.md > 10,000 chars", len(sp_text) > 10000, f"actual: {len(sp_text)}")
|
|
314
|
+
check(51, "system_prompt.md contains orchestrator content (## Role or ## Goals)",
|
|
315
|
+
"## role" in sp_text.lower() or "## goals" in sp_text.lower(), "")
|
|
316
|
+
check(52, "system_prompt.md contains version header",
|
|
317
|
+
"agents-maker system_prompt.md" in sp_text, sp_text[:200])
|
|
318
|
+
check(53, "system_prompt.md contains [Companion] instruction",
|
|
319
|
+
"[Companion]" in sp_text, sp_text[:500])
|
|
320
|
+
else:
|
|
321
|
+
for n in range(50, 54):
|
|
322
|
+
check(n, "system_prompt.md content check", False, "file missing")
|
|
323
|
+
|
|
324
|
+
# -------------------------------------------------------------
|
|
325
|
+
# K. generate_prompt.py — --compress with all phases
|
|
326
|
+
# -------------------------------------------------------------
|
|
327
|
+
section("K. --compress with all phases")
|
|
328
|
+
|
|
329
|
+
compress_phases = ["task_framing", "implementation", "review_refinement", "handoff", "solution_design"]
|
|
330
|
+
for i, ph in enumerate(compress_phases):
|
|
331
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "some task", "--phase", ph, "--compress"])
|
|
332
|
+
n = 54 + i
|
|
333
|
+
check(n, f"--compress --phase {ph} -> exits 0 + Output Policy in output",
|
|
334
|
+
rc == 0 and "## Output Policy" in out,
|
|
335
|
+
f"exit={rc} stderr={err[:100]}")
|
|
336
|
+
|
|
337
|
+
# -------------------------------------------------------------
|
|
338
|
+
# L. --full + --compress combined
|
|
339
|
+
# -------------------------------------------------------------
|
|
340
|
+
section("L. --full + --compress combined")
|
|
341
|
+
|
|
342
|
+
rc, out, err = run([PY, "tools/generate_prompt.py", "add feature", "--full", "--compress"])
|
|
343
|
+
check(59, "--full --compress combined -> exits 0", rc == 0, err[:200])
|
|
344
|
+
check(60, "--full --compress -> contains both system prompt and Output Policy",
|
|
345
|
+
("Orchestrator" in out or "orchestrator" in out) and "## Output Policy" in out, "")
|
|
346
|
+
|
|
347
|
+
# -------------------------------------------------------------
|
|
348
|
+
# M. generate_claude_agents.py — subagents + slash commands
|
|
349
|
+
# -------------------------------------------------------------
|
|
350
|
+
section("M. generate_claude_agents.py — .claude/ subagents + commands")
|
|
351
|
+
|
|
352
|
+
rc, out, err = run([PY, "tools/generate_claude_agents.py", "--template", "--dry-run"])
|
|
353
|
+
check(61, "generate_claude_agents --dry-run exits 0", rc == 0, err[:200])
|
|
354
|
+
check(62, "dry-run reports 20 files (10 agents + 10 commands)",
|
|
355
|
+
out.count("agents/") >= 10 and out.count("commands/") >= 10, out[:300])
|
|
356
|
+
|
|
357
|
+
claude_agents_dir = ROOT / "claude" / "agents"
|
|
358
|
+
claude_cmds_dir = ROOT / "claude" / "commands"
|
|
359
|
+
agent_files = sorted(claude_agents_dir.glob("*.md")) if claude_agents_dir.is_dir() else []
|
|
360
|
+
cmd_files = sorted(claude_cmds_dir.glob("*.md")) if claude_cmds_dir.is_dir() else []
|
|
361
|
+
check(63, "committed claude/ templates: 10 agents + 10 commands",
|
|
362
|
+
len(agent_files) == 10 and len(cmd_files) == 10,
|
|
363
|
+
f"agents={len(agent_files)} commands={len(cmd_files)}")
|
|
364
|
+
|
|
365
|
+
pp = claude_agents_dir / "planpro.md"
|
|
366
|
+
pp_text = pp.read_text(encoding="utf-8") if pp.exists() else ""
|
|
367
|
+
check(64, "subagent planpro.md has valid frontmatter (name/tools/model)",
|
|
368
|
+
"name: planpro" in pp_text and "tools:" in pp_text and "model: inherit" in pp_text,
|
|
369
|
+
pp_text[:200])
|
|
370
|
+
|
|
371
|
+
brain_cmd = claude_cmds_dir / "brain.md"
|
|
372
|
+
bc_text = brain_cmd.read_text(encoding="utf-8") if brain_cmd.exists() else ""
|
|
373
|
+
check(65, "command brain.md has description + $ARGUMENTS",
|
|
374
|
+
"description:" in bc_text and "$ARGUMENTS" in bc_text, bc_text[:200])
|
|
375
|
+
|
|
376
|
+
# -------------------------------------------------------------
|
|
377
|
+
# Summary
|
|
378
|
+
# -------------------------------------------------------------
|
|
379
|
+
total = PASS_COUNT + FAIL_COUNT
|
|
380
|
+
print(f"\n{'=' * 60}")
|
|
381
|
+
print(f" RESULTS: {PASS_COUNT}/{total} tests passed | {FAIL_COUNT} failed")
|
|
382
|
+
print('=' * 60)
|
|
383
|
+
|
|
384
|
+
if FAIL_COUNT > 0:
|
|
385
|
+
print("\nFailed tests:")
|
|
386
|
+
for n, passed, desc, reason in RESULTS:
|
|
387
|
+
if not passed:
|
|
388
|
+
print(f" [{n:02d}] {desc}")
|
|
389
|
+
if reason:
|
|
390
|
+
print(f" {reason[:300]}")
|
|
391
|
+
|
|
392
|
+
sys.exit(0 if FAIL_COUNT == 0 else 1)
|