@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.
Files changed (68) hide show
  1. package/PROMPT_TEMPLATE.md +143 -0
  2. package/README.md +92 -25
  3. package/agents/brain.md +90 -0
  4. package/agents/orchestrator.md +24 -3
  5. package/agents/planpro.md +91 -0
  6. package/bin/cli.js +40 -4
  7. package/claude/agents/architect.md +182 -0
  8. package/claude/agents/brain.md +97 -0
  9. package/claude/agents/code.md +185 -0
  10. package/claude/agents/compress.md +233 -0
  11. package/claude/agents/execute.md +164 -0
  12. package/claude/agents/orchestrate.md +434 -0
  13. package/claude/agents/planpro.md +98 -0
  14. package/claude/agents/review.md +141 -0
  15. package/claude/agents/ui.md +154 -0
  16. package/claude/agents/ux.md +151 -0
  17. package/claude/commands/architect.md +15 -0
  18. package/claude/commands/brain.md +15 -0
  19. package/claude/commands/code.md +15 -0
  20. package/claude/commands/compress.md +15 -0
  21. package/claude/commands/execute.md +15 -0
  22. package/claude/commands/orchestrate.md +15 -0
  23. package/claude/commands/planpro.md +15 -0
  24. package/claude/commands/review.md +15 -0
  25. package/claude/commands/ui.md +15 -0
  26. package/claude/commands/ux.md +15 -0
  27. package/config/agents.yaml +56 -0
  28. package/context_loaders/project_summary.py +5 -0
  29. package/docs/architecture.md +318 -0
  30. package/docs/domains.md +154 -0
  31. package/docs/workflows.md +548 -0
  32. package/examples/generic_project_lifecycle.md +518 -0
  33. package/examples/proof/README.md +138 -0
  34. package/examples/proof/implement-a-python-function-that-parses-an-iso-8/grade.md +14 -0
  35. package/examples/proof/implement-a-python-function-that-parses-an-iso-8/naive_output.md +211 -0
  36. package/examples/proof/implement-a-python-function-that-parses-an-iso-8/naive_prompt.txt +1 -0
  37. package/examples/proof/implement-a-python-function-that-parses-an-iso-8/structured_output.md +210 -0
  38. package/examples/proof/implement-a-python-function-that-parses-an-iso-8/structured_user.txt +35 -0
  39. package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/grade.md +14 -0
  40. package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/naive_output.md +274 -0
  41. package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/naive_prompt.txt +1 -0
  42. package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/structured_output.md +127 -0
  43. package/examples/proof/write-a-runbook-for-recovering-from-a-redis-prim/structured_user.txt +35 -0
  44. package/package.json +10 -2
  45. package/platforms/antigravity.md +195 -0
  46. package/platforms/claude.md +305 -0
  47. package/platforms/openai.md +333 -0
  48. package/skills/analyze_repo.md +86 -86
  49. package/token_optimization/compressor.py +4 -7
  50. package/tools/_core.py +102 -0
  51. package/tools/compare_prompts.py +196 -0
  52. package/tools/generate_claude_agents.py +162 -0
  53. package/tools/generate_claude_md.py +14 -80
  54. package/tools/generate_platform_configs.py +26 -36
  55. package/tools/generate_prompt.py +79 -79
  56. package/tools/grade_proof.py +118 -0
  57. package/tools/init_project.py +11 -58
  58. package/tools/routing.py +103 -0
  59. package/tools/test_kit.py +392 -363
  60. package/tools/validate_kit.py +15 -43
  61. package/context_loaders/__pycache__/__init__.cpython-314.pyc +0 -0
  62. package/context_loaders/__pycache__/file_chunker.cpython-314.pyc +0 -0
  63. package/context_loaders/__pycache__/project_summary.cpython-314.pyc +0 -0
  64. package/context_loaders/__pycache__/repo_tree.cpython-314.pyc +0 -0
  65. package/token_optimization/__pycache__/compressor.cpython-314.pyc +0 -0
  66. package/tools/__pycache__/domain_utils.cpython-314.pyc +0 -0
  67. package/tools/__pycache__/generate_claude_md.cpython-314.pyc +0 -0
  68. package/tools/__pycache__/validate_kit.cpython-314.pyc +0 -0
@@ -14,10 +14,8 @@ Usage:
14
14
  from __future__ import annotations
15
15
 
16
16
  import argparse
17
- import os
18
17
  import re
19
18
  import sys
20
- import tempfile
21
19
  from pathlib import Path
22
20
 
23
21
  __version__ = "1.0.0"
@@ -27,25 +25,21 @@ KIT_DIR = SCRIPT_DIR.parent # agents-maker/
27
25
  sys.path.insert(0, str(KIT_DIR))
28
26
 
29
27
  try:
30
- import yaml
28
+ from tools._core import atomic_write, load_yaml, py_invocation
29
+ from tools.routing import AGENT_ROLES, PHASE_AGENTS, PHASE_LABELS, phase_agents
31
30
  except ImportError:
32
- print("[ERROR] pyyaml is required: pip install pyyaml", file=sys.stderr)
33
- sys.exit(1)
31
+ from _core import atomic_write, load_yaml, py_invocation
32
+ from routing import AGENT_ROLES, PHASE_AGENTS, PHASE_LABELS, phase_agents
33
+
34
+ # Backward-compatible aliases (generate_platform_configs imports these names).
35
+ _PHASE_AGENTS = PHASE_AGENTS
36
+ _PHASE_LABELS = PHASE_LABELS
37
+ _AGENT_ROLES = AGENT_ROLES
34
38
 
35
39
  # ---------------------------------------------------------------------------
36
40
  # Helpers
37
41
  # ---------------------------------------------------------------------------
38
42
 
39
- def _load_yaml(path: Path) -> dict:
40
- if not path.exists():
41
- return {}
42
- try:
43
- with open(path, encoding="utf-8") as f:
44
- return yaml.safe_load(f) or {}
45
- except Exception:
46
- return {}
47
-
48
-
49
43
  def _parse_phase(state_text: str) -> str:
50
44
  m = re.search(r"##\s+Current Phase\s*\n+(\S+)", state_text)
51
45
  if m:
@@ -53,65 +47,8 @@ def _parse_phase(state_text: str) -> str:
53
47
  return "task_framing"
54
48
 
55
49
 
56
- # Phase → domain → active agents mapping (derived from agents.yaml lifecycle)
57
- _PHASE_AGENTS: dict[str, dict[str, list[str]]] = {
58
- "task_framing": {"_all": ["orchestrator"]},
59
- "requirements": {"_all": ["orchestrator", "architect_agent"]},
60
- "solution_design": {
61
- "_all": ["architect_agent"],
62
- "software": ["architect_agent", "ui_agent"],
63
- "product_design":["architect_agent", "ui_agent", "ux_agent"],
64
- "marketing": ["architect_agent", "ux_agent"],
65
- },
66
- "implementation": {
67
- "_all": ["execution_agent"],
68
- "software": ["code_agent"],
69
- "data_analytics":["code_agent"],
70
- },
71
- "review_refinement":{
72
- "_all": ["reviewer_agent"],
73
- "software": ["reviewer_agent", "code_agent"],
74
- "product_design":["reviewer_agent", "ui_agent", "ux_agent"],
75
- "marketing": ["reviewer_agent", "ux_agent"],
76
- },
77
- "handoff": {"_all": ["orchestrator", "execution_agent"]},
78
- }
79
-
80
- _PHASE_LABELS: dict[str, str] = {
81
- "task_framing": "Task Framing",
82
- "requirements": "Requirements",
83
- "solution_design": "Solution Design",
84
- "implementation": "Implementation",
85
- "review_refinement":"Review & Refinement",
86
- "handoff": "Handoff",
87
- # aliases accepted by generate_prompt.py
88
- "framing": "Task Framing",
89
- "design": "Solution Design",
90
- "implement": "Implementation",
91
- "review": "Review & Refinement",
92
- }
93
-
94
- _AGENT_ROLES: dict[str, str] = {
95
- "orchestrator": "routing",
96
- "architect_agent": "design",
97
- "code_agent": "implementation",
98
- "execution_agent": "execution",
99
- "ui_agent": "UI",
100
- "ux_agent": "UX",
101
- "reviewer_agent": "QA",
102
- "compression_agent": "compression",
103
- }
104
-
105
-
106
- def _py(kit_rel: str, tool: str) -> str:
107
- """Return a shell-safe 'python ...' invocation. Quotes the full path when it contains spaces."""
108
- path = f"{kit_rel}/tools/{tool}"
109
- return f'python "{path}"' if " " in path else f"python {path}"
110
-
111
-
112
50
  def _active_agents(domain: str, phase: str) -> list[str]:
113
- phase_map = _PHASE_AGENTS.get(phase, {"_all": ["orchestrator"]})
114
- return phase_map.get(domain, phase_map["_all"])
51
+ return phase_agents(phase, domain)
115
52
 
116
53
 
117
54
  # ---------------------------------------------------------------------------
@@ -132,8 +69,8 @@ def build_claude_md(
132
69
  agent_list = ", ".join(
133
70
  f"{a} ({_AGENT_ROLES.get(a, 'specialist')})" for a in agents
134
71
  )
135
- regen_cmd = _py(kit_rel_path, "generate_claude_md.py")
136
- prompt_cmd = _py(kit_rel_path, "generate_prompt.py")
72
+ regen_cmd = py_invocation(kit_rel_path, "generate_claude_md.py")
73
+ prompt_cmd = py_invocation(kit_rel_path, "generate_prompt.py")
137
74
 
138
75
  return (
139
76
  f"# agents-maker — Project AI Config\n"
@@ -207,7 +144,7 @@ def main() -> None:
207
144
  sys.exit(1)
208
145
 
209
146
  # Load project config
210
- project_cfg = _load_yaml(KIT_DIR / "config" / "project.yaml")
147
+ project_cfg = load_yaml(KIT_DIR / "config" / "project.yaml")
211
148
  if not project_cfg:
212
149
  print(
213
150
  "[WARN] config/project.yaml not found — run init_project.py first.",
@@ -250,10 +187,7 @@ def main() -> None:
250
187
  return
251
188
 
252
189
  out_path = project_root / "CLAUDE.md"
253
- with tempfile.NamedTemporaryFile("w", dir=out_path.parent, delete=False, suffix=".tmp", encoding="utf-8") as f:
254
- f.write(content)
255
- tmp = f.name
256
- os.replace(tmp, out_path)
190
+ atomic_write(out_path, content)
257
191
 
258
192
  print(f"\nWritten: {out_path}")
259
193
  print(f" Domain : {domain} (confidence: {confidence})")
@@ -24,9 +24,7 @@ from __future__ import annotations
24
24
  __version__ = "1.0.0"
25
25
 
26
26
  import argparse
27
- import os
28
27
  import sys
29
- import tempfile
30
28
  from pathlib import Path
31
29
 
32
30
  SCRIPT_DIR = Path(__file__).resolve().parent # agents-maker/tools/
@@ -34,12 +32,7 @@ KIT_DIR = SCRIPT_DIR.parent # agents-maker/
34
32
  sys.path.insert(0, str(KIT_DIR))
35
33
 
36
34
  try:
37
- import yaml as _yaml_probe # noqa: F401 — ensures pyyaml is installed
38
- except ImportError:
39
- print("[ERROR] pyyaml is required: pip install pyyaml", file=sys.stderr)
40
- sys.exit(1)
41
-
42
- try:
35
+ from tools._core import atomic_write, load_yaml, py_invocation
43
36
  from tools.generate_claude_md import (
44
37
  _AGENT_ROLES,
45
38
  _PHASE_AGENTS,
@@ -48,6 +41,7 @@ try:
48
41
  build_claude_md,
49
42
  )
50
43
  except ImportError:
44
+ from _core import atomic_write, load_yaml, py_invocation
51
45
  from generate_claude_md import (
52
46
  _AGENT_ROLES,
53
47
  _PHASE_AGENTS,
@@ -56,12 +50,7 @@ except ImportError:
56
50
  build_claude_md,
57
51
  )
58
52
 
59
- try:
60
- from tools.domain_utils import _load_yaml
61
- except ImportError:
62
- from domain_utils import _load_yaml
63
-
64
- PLATFORMS = ["claude", "copilot", "cursor", "antigravity"]
53
+ PLATFORMS = ["claude", "claude_agents", "copilot", "cursor", "antigravity"]
65
54
 
66
55
  # ---------------------------------------------------------------------------
67
56
  # Helpers shared across builders
@@ -76,12 +65,6 @@ def _agent_list_str(agents: list[str]) -> str:
76
65
  return ", ".join(f"{a} ({_AGENT_ROLES.get(a, 'specialist')})" for a in agents)
77
66
 
78
67
 
79
- def _py(kit_rel: str, tool: str) -> str:
80
- """Return a shell-safe 'python ...' invocation. Quotes the full path when it contains spaces."""
81
- path = f"{kit_rel}/tools/{tool}"
82
- return f'python "{path}"' if " " in path else f"python {path}"
83
-
84
-
85
68
  def _yaml_str(value: str) -> str:
86
69
  """Return a YAML-safe scalar: quoted if it contains spaces or YAML special characters."""
87
70
  if " " in value or any(c in value for c in ":{}[]#&*!|>'\"%@`"):
@@ -89,16 +72,6 @@ def _yaml_str(value: str) -> str:
89
72
  return value
90
73
 
91
74
 
92
- def _atomic_write(path: Path, content: str) -> None:
93
- path.parent.mkdir(parents=True, exist_ok=True)
94
- with tempfile.NamedTemporaryFile(
95
- "w", dir=path.parent, delete=False, suffix=".tmp", encoding="utf-8"
96
- ) as f:
97
- f.write(content)
98
- tmp = f.name
99
- os.replace(tmp, path)
100
-
101
-
102
75
  # ---------------------------------------------------------------------------
103
76
  # Builder: GitHub Copilot — .github/copilot-instructions.md
104
77
  # ---------------------------------------------------------------------------
@@ -114,7 +87,7 @@ def build_copilot_md(
114
87
  stack_str = ", ".join(stack) if stack else "unknown"
115
88
  phase_label = _PHASE_LABELS.get(phase, phase)
116
89
  agents = _active_agents(domain, phase)
117
- regen_cmd = _py(kit_rel_path, "generate_platform_configs.py")
90
+ regen_cmd = py_invocation(kit_rel_path, "generate_platform_configs.py")
118
91
 
119
92
  all_agents = [
120
93
  "orchestrator (routing — always active)",
@@ -173,7 +146,7 @@ def build_cursor_rules(
173
146
  stack_str = ", ".join(stack) if stack else "unknown"
174
147
  phase_label = _PHASE_LABELS.get(phase, phase)
175
148
  agents = _active_agents(domain, phase)
176
- regen_cmd = _py(kit_rel_path, "generate_platform_configs.py")
149
+ regen_cmd = py_invocation(kit_rel_path, "generate_platform_configs.py")
177
150
 
178
151
  return (
179
152
  f"# agents-maker — Cursor Rules\n"
@@ -219,6 +192,8 @@ _AGENT_DESCRIPTIONS: dict[str, str] = {
219
192
  "ux_agent": "Flow critique, onboarding sequences, funnel analysis, friction ID",
220
193
  "reviewer_agent": "Severity-rated QA review for any domain (CRITICAL/HIGH/MEDIUM/LOW)",
221
194
  "compression_agent": "Token budget enforcement, context compression, cross-session resumption",
195
+ "brain": "Project brainstorming — 3+ approaches with trade-offs + a recommendation",
196
+ "planpro": "Implementation planning — short, specific, dependency-ordered plan file",
222
197
  }
223
198
 
224
199
  _AGENT_PHASES: dict[str, list[str]] = {
@@ -230,6 +205,8 @@ _AGENT_PHASES: dict[str, list[str]] = {
230
205
  "ux_agent": ["solution_design", "review_refinement"],
231
206
  "reviewer_agent": ["review_refinement"],
232
207
  "compression_agent": ["handoff"],
208
+ "brain": ["task_framing", "solution_design"],
209
+ "planpro": ["task_framing", "requirements", "solution_design"],
233
210
  }
234
211
 
235
212
  _AGENT_DOMAINS: dict[str, list[str]] = {
@@ -241,10 +218,12 @@ _AGENT_DOMAINS: dict[str, list[str]] = {
241
218
  "ux_agent": ["product_design", "marketing"],
242
219
  "reviewer_agent": ["software", "content", "research", "data_analytics", "product_design", "marketing", "ops_process", "general"],
243
220
  "compression_agent": ["software", "content", "research", "data_analytics", "product_design", "marketing", "ops_process", "general"],
221
+ "brain": ["software", "content", "research", "data_analytics", "product_design", "marketing", "ops_process", "general"],
222
+ "planpro": ["software", "content", "research", "data_analytics", "product_design", "marketing", "ops_process", "general"],
244
223
  }
245
224
 
246
225
  _AGENT_ORDER = [
247
- "orchestrator", "architect_agent", "code_agent", "execution_agent",
226
+ "orchestrator", "brain", "planpro", "architect_agent", "code_agent", "execution_agent",
248
227
  "ui_agent", "ux_agent", "reviewer_agent", "compression_agent",
249
228
  ]
250
229
 
@@ -256,7 +235,7 @@ def build_agkit_yaml(
256
235
  phase: str,
257
236
  kit_rel_path: str,
258
237
  ) -> str:
259
- regen_cmd = _py(kit_rel_path, "generate_platform_configs.py")
238
+ regen_cmd = py_invocation(kit_rel_path, "generate_platform_configs.py")
260
239
  stack_list = stack if stack else ["unknown"]
261
240
 
262
241
  lines: list[str] = [
@@ -346,7 +325,7 @@ def generate_all(
346
325
  platforms: list[str],
347
326
  dry_run: bool,
348
327
  ) -> None:
349
- project_cfg = _load_yaml(kit_dir / "config" / "project.yaml")
328
+ project_cfg = load_yaml(kit_dir / "config" / "project.yaml")
350
329
  if not project_cfg:
351
330
  print("[WARN] config/project.yaml not found — run init_project.py first.", file=sys.stderr)
352
331
 
@@ -384,6 +363,17 @@ def generate_all(
384
363
  content = build_agkit_yaml(project_name, domain, stack, phase, kit_rel_path)
385
364
  builders["antigravity"] = (_PLATFORM_PATHS["antigravity"], content)
386
365
 
366
+ # Claude Code subagents + slash commands (.claude/agents, .claude/commands).
367
+ if "claude_agents" in platforms:
368
+ try:
369
+ from tools.generate_claude_agents import generate as _gen_claude
370
+ except ImportError:
371
+ from generate_claude_agents import generate as _gen_claude
372
+ cw, cs = _gen_claude(project_root / ".claude", kit_dir, force=False, dry_run=dry_run)
373
+ tag = "[dry-run] would write" if dry_run else " [DONE]"
374
+ print(f"{tag} {len(cw)} .claude/ file(s) — subagents + slash commands"
375
+ + (f"; kept {len(cs)} existing" if cs else ""))
376
+
387
377
  if dry_run:
388
378
  for platform, (rel_path, content) in builders.items():
389
379
  print(f"\n{'='*60}")
@@ -397,7 +387,7 @@ def generate_all(
397
387
  for platform, (rel_path, content) in builders.items():
398
388
  out_path = project_root / rel_path
399
389
  try:
400
- _atomic_write(out_path, content)
390
+ atomic_write(out_path, content)
401
391
  print(f" [DONE] {rel_path} ({platform})")
402
392
  written.append(rel_path)
403
393
  except OSError as e:
@@ -21,10 +21,8 @@ Usage:
21
21
  from __future__ import annotations
22
22
 
23
23
  import argparse
24
- import os
25
24
  import re
26
25
  import sys
27
- import tempfile
28
26
  from datetime import date
29
27
  from pathlib import Path
30
28
 
@@ -36,12 +34,6 @@ SCRIPT_DIR = Path(__file__).resolve().parent # agents-maker/tools/
36
34
  KIT_DIR = SCRIPT_DIR.parent # agents-maker/
37
35
  sys.path.insert(0, str(KIT_DIR))
38
36
 
39
- try:
40
- import yaml
41
- except ImportError:
42
- print("[ERROR] pyyaml is required: pip install pyyaml", file=sys.stderr)
43
- sys.exit(1)
44
-
45
37
  __version__ = "1.0.0"
46
38
 
47
39
  MAX_PROBLEM_LENGTH = 5000
@@ -51,11 +43,13 @@ MAX_PROBLEM_LENGTH = 5000
51
43
  # ---------------------------------------------------------------------------
52
44
 
53
45
  try:
54
- from tools.domain_utils import _load_yaml
46
+ from tools._core import atomic_write_yaml, load_yaml
55
47
  from tools.domain_utils import detect_domain as _du_detect
48
+ from tools.routing import active_agents, domain_agents
56
49
  except ImportError:
57
- from domain_utils import _load_yaml
50
+ from _core import atomic_write_yaml, load_yaml
58
51
  from domain_utils import detect_domain as _du_detect
52
+ from routing import active_agents, domain_agents
59
53
 
60
54
 
61
55
  def detect_domain(problem: str) -> tuple[str, str, float]:
@@ -118,53 +112,14 @@ def infer_phase(state_path: Path) -> str:
118
112
  # Agent selection per phase × domain
119
113
  # ---------------------------------------------------------------------------
120
114
 
121
- PHASE_AGENTS: dict[str, list[str]] = {
122
- "task_framing": ["orchestrator"],
123
- "requirements": ["orchestrator", "architect_agent"],
124
- "solution_design": ["orchestrator", "architect_agent"],
125
- "implementation": ["orchestrator"],
126
- "review_refinement": ["orchestrator", "reviewer_agent"],
127
- "handoff": ["orchestrator"],
128
- }
129
-
130
- IMPLEMENTATION_AGENT: dict[str, str] = {
131
- "software": "code_agent",
132
- "data_analytics": "code_agent",
133
- "content": "execution_agent",
134
- "research": "execution_agent",
135
- "product_design": "execution_agent",
136
- "marketing": "execution_agent",
137
- "ops_process": "execution_agent",
138
- "general": "execution_agent",
139
- }
140
-
141
- DESIGN_AGENTS: dict[str, list[str]] = {
142
- "product_design": ["ui_agent", "ux_agent"],
143
- "marketing": ["ux_agent"],
144
- }
145
-
146
-
147
115
  def select_agents(phase: str, domain: str) -> list[str]:
148
- agents = list(PHASE_AGENTS.get(phase, ["orchestrator"]))
149
-
150
- if phase == "implementation":
151
- impl_agent = IMPLEMENTATION_AGENT.get(domain, "execution_agent")
152
- if impl_agent not in agents:
153
- agents.append(impl_agent)
154
- for extra in DESIGN_AGENTS.get(domain, []):
155
- if extra not in agents:
156
- agents.append(extra)
157
-
158
- if phase == "solution_design":
159
- for extra in DESIGN_AGENTS.get(domain, []):
160
- if extra not in agents:
161
- agents.append(extra)
162
-
163
- return agents
116
+ # Routing is centralized in tools/routing.py (single source of truth) so the
117
+ # generated prompt, CLAUDE.md, and platform configs all agree.
118
+ return active_agents(phase, domain)
164
119
 
165
120
 
166
121
  def select_skills(agents: list[str]) -> list[str]:
167
- agents_cfg = _load_yaml(KIT_DIR / "config" / "agents.yaml").get("agents", {})
122
+ agents_cfg = load_yaml(KIT_DIR / "config" / "agents.yaml").get("agents", {})
168
123
  seen: set[str] = set()
169
124
  skills: list[str] = []
170
125
  for agent in agents:
@@ -199,6 +154,42 @@ def load_skill_md(skill_key: str) -> str:
199
154
  return ""
200
155
 
201
156
 
157
+ # ---------------------------------------------------------------------------
158
+ # Task-scoped system prompt (lazy-load: only the routed agents + skills)
159
+ # ---------------------------------------------------------------------------
160
+
161
+ COMPANION_DIRECTIVE = (
162
+ "# agents-maker — active kit (task-scoped)\n"
163
+ "You are operating under the agents-maker system below, loaded with only the "
164
+ "agents and skills routed for this task. The Orchestrator leads.\n"
165
+ "After every response append a [Companion] block:\n"
166
+ " ---\n"
167
+ " [Companion] Phase: <phase> | Domain: <domain> | Est. token budget used: ~N%\n"
168
+ " What to do next (pick one):\n"
169
+ " [Recommended] A: <action> Command: python agents-maker/tools/generate_prompt.py \"...\"\n"
170
+ " B: <action>\n"
171
+ " C: <action>\n"
172
+ " ---\n"
173
+ "In Direct Task Mode (a single self-contained task) deliver the finished artifact "
174
+ "first, then the [Companion] block — do not open the phase lifecycle."
175
+ )
176
+
177
+
178
+ def build_scoped_system(agents: list[str], skills: list[str]) -> str:
179
+ """Assemble a task-scoped system prompt: companion directive + only the routed
180
+ agent and skill specs. Far smaller than the full 8-agent / 12-skill system_prompt.md."""
181
+ parts: list[str] = [COMPANION_DIRECTIVE, "=" * 60]
182
+ for agent in agents:
183
+ parts.append(load_agent_md(agent))
184
+ parts.append("---")
185
+ for skill in skills:
186
+ content = load_skill_md(skill)
187
+ if content:
188
+ parts.append(content)
189
+ parts.append("---")
190
+ return "\n\n".join(parts)
191
+
192
+
202
193
  # ---------------------------------------------------------------------------
203
194
  # Token estimation
204
195
  # ---------------------------------------------------------------------------
@@ -223,28 +214,16 @@ def build_prompt(
223
214
  project_cfg: dict,
224
215
  state_text: str,
225
216
  include_system: bool = False,
217
+ system_agents: list[str] | None = None,
218
+ system_skills: list[str] | None = None,
226
219
  ) -> str:
227
220
  parts: list[str] = []
228
221
 
229
222
  if include_system:
230
- sys_path = KIT_DIR / "system_prompt.md"
231
- if sys_path.exists():
232
- try:
233
- parts.append(sys_path.read_text(encoding="utf-8").strip())
234
- parts.append("\n" + "=" * 60 + "\n")
235
- except (OSError, PermissionError):
236
- print("[WARN] Could not read system_prompt.md — inlining agents instead.", file=sys.stderr)
237
- include_system = False
238
-
239
- if not sys_path.exists() or not include_system:
240
- for agent in agents:
241
- parts.append(load_agent_md(agent))
242
- parts.append("---")
243
- for skill in skills:
244
- content = load_skill_md(skill)
245
- if content:
246
- parts.append(content)
247
- parts.append("---")
223
+ # Task-scoped: inline only the domain's routed agents + skills (not the full
224
+ # 8-agent / 12-skill system_prompt.md) to keep token cost low.
225
+ parts.append(build_scoped_system(system_agents or agents, system_skills or skills))
226
+ parts.append("\n" + "=" * 60 + "\n")
248
227
 
249
228
  project_name = project_cfg.get("project_name", "unknown")
250
229
  proj_domain = project_cfg.get("primary_domain", domain)
@@ -266,7 +245,13 @@ def build_prompt(
266
245
  session_num = project_cfg.get("session_count", 0) + 1
267
246
  parts.append(f"## Session State\nSession {session_num} — starting fresh")
268
247
 
269
- parts.append(f"## Task\n{problem}")
248
+ parts.append(
249
+ f"## Task\n{problem}\n\n"
250
+ "Execute this task now. If it is self-contained, deliver the finished artifact "
251
+ "immediately in Direct Task Mode — do not greet, do not restate this configuration, "
252
+ "do not ask for project context, and do not open the phase lifecycle unless the task "
253
+ "is clearly multi-phase. End with the [Companion] next-steps block."
254
+ )
270
255
 
271
256
  routing_lines = [
272
257
  "## Domain & Routing",
@@ -320,6 +305,11 @@ def main() -> None:
320
305
  action="store_true",
321
306
  help="Apply token policy for the detected phase/domain: appends output style instruction and reports token budget.",
322
307
  )
308
+ parser.add_argument(
309
+ "--system-only",
310
+ action="store_true",
311
+ help="Print only the task-scoped system prompt (routed agents + skills) and exit. Use as the `system` field of an API call.",
312
+ )
323
313
  args = parser.parse_args()
324
314
 
325
315
  # Validate problem statement
@@ -332,7 +322,7 @@ def main() -> None:
332
322
 
333
323
  # Load project config
334
324
  project_yaml_path = KIT_DIR / "config" / "project.yaml"
335
- project_cfg = _load_yaml(project_yaml_path)
325
+ project_cfg = load_yaml(project_yaml_path)
336
326
  if not project_cfg:
337
327
  print(
338
328
  "[WARN] project.yaml not found or empty. Run init_project.py first for best results.",
@@ -378,6 +368,19 @@ def main() -> None:
378
368
  agents = select_agents(phase, domain)
379
369
  skills = select_skills(agents)
380
370
 
371
+ # A self-contained system prompt (--full / --system-only) must carry the whole
372
+ # domain's specialists so Direct Task Mode can deliver — scope by domain, not phase.
373
+ if args.full or args.system_only:
374
+ sys_agents = domain_agents(domain)
375
+ sys_skills = select_skills(sys_agents)
376
+ else:
377
+ sys_agents, sys_skills = agents, skills
378
+
379
+ # --system-only: emit just the task-scoped system prompt (for the API `system` field).
380
+ if args.system_only:
381
+ print(build_scoped_system(sys_agents, sys_skills))
382
+ return
383
+
381
384
  prompt_text = build_prompt(
382
385
  problem=args.problem,
383
386
  domain=domain,
@@ -389,6 +392,8 @@ def main() -> None:
389
392
  project_cfg=project_cfg,
390
393
  state_text=state_text,
391
394
  include_system=args.full,
395
+ system_agents=sys_agents,
396
+ system_skills=sys_skills,
392
397
  )
393
398
 
394
399
  # Apply token policy when --compress is requested
@@ -447,12 +452,7 @@ def main() -> None:
447
452
  project_cfg["session_count"] = project_cfg.get("session_count", 0) + 1
448
453
  project_cfg["last_session"] = date.today().isoformat()
449
454
  try:
450
- with tempfile.NamedTemporaryFile(
451
- "w", dir=project_yaml_path.parent, delete=False, suffix=".tmp", encoding="utf-8"
452
- ) as _f:
453
- yaml.dump(project_cfg, _f, default_flow_style=False, allow_unicode=True, sort_keys=False)
454
- _tmp = _f.name
455
- os.replace(_tmp, project_yaml_path)
455
+ atomic_write_yaml(project_yaml_path, project_cfg)
456
456
  except (OSError, PermissionError) as e:
457
457
  print(f"[WARN] Could not update session_count in project.yaml: {e}", file=sys.stderr)
458
458
 
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ grade_proof.py — Blind adversarial grading for compare_prompts.py output.
4
+
5
+ Takes a proof folder (naive_prompt.txt + naive_output.md + structured_output.md),
6
+ anonymizes the two responses as A/B (order decided by a stable per-folder seed,
7
+ so it is blind but reproducible), and asks an impartial judge model to score both
8
+ on completeness / correctness / actionability / structure and pick a winner.
9
+
10
+ The judge is a DIFFERENT model from the one that produced the outputs, to reduce
11
+ self-preference bias. Judge input excludes the system prompt, so it stays well
12
+ under tight input-token rate limits.
13
+
14
+ Usage:
15
+ export ANTHROPIC_API_KEY=sk-ant-...
16
+ python tools/grade_proof.py examples/proof/<folder>
17
+ python tools/grade_proof.py examples/proof/<folder> --judge claude-sonnet-5
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import hashlib
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ import urllib.error
29
+ import urllib.request
30
+ from pathlib import Path
31
+
32
+ CRITERIA = ["completeness", "correctness", "actionability", "structure"]
33
+
34
+
35
+ def call_judge(prompt: str, model: str, max_tokens: int = 1200) -> str:
36
+ key = os.environ.get("ANTHROPIC_API_KEY")
37
+ if not key:
38
+ raise RuntimeError("ANTHROPIC_API_KEY is not set.")
39
+ body = {"model": model, "max_tokens": max_tokens, "messages": [{"role": "user", "content": prompt}]}
40
+ req = urllib.request.Request(
41
+ "https://api.anthropic.com/v1/messages",
42
+ data=json.dumps(body).encode("utf-8"), method="POST",
43
+ headers={"x-api-key": key, "anthropic-version": "2023-06-01", "content-type": "application/json"},
44
+ )
45
+ try:
46
+ with urllib.request.urlopen(req, timeout=120) as resp:
47
+ data = json.loads(resp.read().decode("utf-8"))
48
+ except urllib.error.HTTPError as e:
49
+ raise RuntimeError(f"judge api error {e.code}: {e.read().decode('utf-8','replace')[:300]}") from None
50
+ return "".join(b.get("text", "") for b in data.get("content", [])).strip()
51
+
52
+
53
+ def build_prompt(task: str, resp_a: str, resp_b: str) -> str:
54
+ return (
55
+ "You are a strict, impartial evaluator. A user made this request:\n\n"
56
+ f"<request>\n{task}\n</request>\n\n"
57
+ "Two assistants answered. Judge only how well each fulfills the request "
58
+ "(ignore length; longer is not better).\n\n"
59
+ f"### Response A\n{resp_a}\n\n### Response B\n{resp_b}\n\n"
60
+ "Score each response 1-5 on completeness, correctness, actionability, structure. "
61
+ "Then pick the overall winner. Respond with ONLY JSON, no prose:\n"
62
+ '{"A":{"completeness":0,"correctness":0,"actionability":0,"structure":0},'
63
+ '"B":{"completeness":0,"correctness":0,"actionability":0,"structure":0},'
64
+ '"winner":"A|B|tie","reason":"one sentence"}'
65
+ )
66
+
67
+
68
+ def main() -> None:
69
+ sys.stdout.reconfigure(encoding="utf-8")
70
+ ap = argparse.ArgumentParser(description="Blind-grade a compare_prompts.py proof folder.")
71
+ ap.add_argument("folder", help="Proof folder containing naive_/structured_ files.")
72
+ ap.add_argument("--judge", default="claude-sonnet-5", help="Judge model id (default: claude-sonnet-5).")
73
+ args = ap.parse_args()
74
+
75
+ d = Path(args.folder)
76
+ task = (d / "naive_prompt.txt").read_text(encoding="utf-8").strip()
77
+ naive = (d / "naive_output.md").read_text(encoding="utf-8").strip()
78
+ structured = (d / "structured_output.md").read_text(encoding="utf-8").strip()
79
+
80
+ # Stable, reproducible blind assignment: structured is A iff folder-hash is even.
81
+ seed = int(hashlib.sha256(d.name.encode()).hexdigest(), 16)
82
+ structured_is_a = (seed % 2 == 0)
83
+ resp_a, resp_b = (structured, naive) if structured_is_a else (naive, structured)
84
+
85
+ raw = call_judge(build_prompt(task, resp_a, resp_b), args.judge)
86
+ m = re.search(r"\{.*\}", raw, re.DOTALL)
87
+ if not m:
88
+ raise RuntimeError(f"Judge did not return JSON:\n{raw[:300]}")
89
+ verdict = json.loads(m.group(0))
90
+
91
+ label = {"A": "structured" if structured_is_a else "naive",
92
+ "B": "naive" if structured_is_a else "structured"}
93
+ winner = label.get(verdict.get("winner", ""), verdict.get("winner", "tie"))
94
+ scores = {label[k]: verdict[k] for k in ("A", "B")}
95
+
96
+ lines = [
97
+ f"# Blind grade — {d.name}",
98
+ "",
99
+ f"- **Judge:** `{args.judge}` (different model than the one that produced the outputs)",
100
+ f"- **Blind mapping:** structured = Response {'A' if structured_is_a else 'B'}",
101
+ f"- **Winner:** **{winner}**",
102
+ f"- **Reason:** {verdict.get('reason','')}",
103
+ "",
104
+ "| Criterion | naive | structured |",
105
+ "|---|---|---|",
106
+ ]
107
+ for c in CRITERIA:
108
+ lines.append(f"| {c} | {scores['naive'].get(c,'?')} | {scores['structured'].get(c,'?')} |")
109
+ n_tot = sum(scores["naive"].get(c, 0) for c in CRITERIA)
110
+ s_tot = sum(scores["structured"].get(c, 0) for c in CRITERIA)
111
+ lines.append(f"| **total** | **{n_tot}** | **{s_tot}** |")
112
+ (d / "grade.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
113
+
114
+ print(f"[graded] {d.name}: winner={winner} | naive={n_tot} structured={s_tot} (judge={args.judge})")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()