Skip to content

Commit 49e8ec5

Browse files
committed
Add context pack builder
1 parent 43e62e0 commit 49e8ec5

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

agentflow/context_builder.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from .git_tools import git_status, project_tree, run_cmd
6+
from .models import Task
7+
from .storage import Store
8+
9+
BINARY_EXTS = {
10+
".png", ".jpg", ".jpeg", ".gif", ".pdf", ".zip", ".jar", ".war", ".class",
11+
".exe", ".dll", ".so", ".dylib", ".ico", ".woff", ".woff2", ".ttf",
12+
}
13+
14+
15+
def detect_commands(root: Path) -> dict[str, str]:
16+
if (root / "pom.xml").exists():
17+
return {"test": "mvn test", "lint": "", "format": ""}
18+
if (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
19+
return {"test": "./gradlew test", "lint": "", "format": ""}
20+
if (root / "package.json").exists():
21+
return {"test": "npm test", "lint": "npm run lint", "format": "npm run format"}
22+
if (root / "pyproject.toml").exists():
23+
return {"test": "pytest", "lint": "ruff check .", "format": "ruff format ."}
24+
return {"test": "", "lint": "", "format": ""}
25+
26+
27+
def read_text_excerpt(path: Path, max_chars: int) -> str:
28+
if path.suffix.lower() in BINARY_EXTS:
29+
return f"<binary file omitted: {path.name}>"
30+
try:
31+
text = path.read_text(encoding="utf-8")
32+
except UnicodeDecodeError:
33+
try:
34+
text = path.read_text(encoding="utf-8", errors="replace")
35+
except OSError as exc:
36+
return f"<failed to read: {exc}>"
37+
except OSError as exc:
38+
return f"<failed to read: {exc}>"
39+
if len(text) > max_chars:
40+
return text[:max_chars] + "\n\n... <truncated by AgentFlowDesk>"
41+
return text
42+
43+
44+
def build_task_brief(task: Task, project_name: str, description: str) -> str:
45+
acceptance = "\n".join(f"- [ ] {item}" for item in task.acceptance) or "- [ ] User review required"
46+
files = "\n".join(f"- `{item}`" for item in task.files) or "- No files specified yet"
47+
return f"""# Task Brief: {task.title}
48+
49+
## Project
50+
51+
**Name:** {project_name}
52+
53+
{description or "No project description configured."}
54+
55+
## Task ID
56+
57+
`{task.id}`
58+
59+
## Goal
60+
61+
{task.goal}
62+
63+
## Relevant files
64+
65+
{files}
66+
67+
## Acceptance criteria
68+
69+
{acceptance}
70+
71+
## Notes
72+
73+
{task.notes or "No extra notes."}
74+
"""
75+
76+
77+
def build_agent_instructions(task: Task) -> str:
78+
return f"""# Agent Instructions
79+
80+
You are working on task `{task.id}`: **{task.title}**.
81+
82+
Follow these rules:
83+
84+
1. Read the task brief before editing.
85+
2. Keep the diff focused on the stated goal.
86+
3. Do not make unrelated refactors.
87+
4. Prefer small, reviewable changes.
88+
5. Run the relevant checks if commands are available.
89+
6. Summarize what changed, what was tested, and any remaining risk.
90+
91+
When you finish, provide:
92+
93+
- Changed files
94+
- Implementation summary
95+
- Tests/checks run
96+
- Known limitations
97+
- Review checklist
98+
"""
99+
100+
101+
def build_relevant_files(root: Path, task: Task, max_chars: int) -> str:
102+
if not task.files:
103+
return "# Relevant Files\n\nNo files were specified for this task.\n"
104+
parts = ["# Relevant Files"]
105+
for item in task.files:
106+
p = (root / item).resolve()
107+
try:
108+
p.relative_to(root.resolve())
109+
except ValueError:
110+
parts.append(f"\n## `{item}`\n\n<skipped: outside project root>")
111+
continue
112+
if not p.exists():
113+
parts.append(f"\n## `{item}`\n\n<missing>")
114+
continue
115+
if p.is_dir():
116+
parts.append(f"\n## `{item}`\n\nDirectory tree:\n\n```text\n{project_tree(p, 80)}\n```")
117+
continue
118+
excerpt = read_text_excerpt(p, max_chars)
119+
parts.append(f"\n## `{item}`\n\n```text\n{excerpt}\n```")
120+
return "\n".join(parts) + "\n"
121+
122+
123+
def build_commands(root: Path, configured: dict[str, str]) -> str:
124+
detected = detect_commands(root)
125+
commands = {**detected, **{k: v for k, v in configured.items() if v}}
126+
lines = ["# Project Commands", ""]
127+
for name in ["test", "lint", "format"]:
128+
value = commands.get(name, "")
129+
lines.append(f"- **{name}:** `{value or '<not configured>'}`")
130+
lines.append("")
131+
return "\n".join(lines)
132+
133+
134+
def build_environment(root: Path, include_tree: bool, include_git: bool) -> str:
135+
parts = ["# Project Environment", ""]
136+
if include_tree:
137+
parts.append("## File tree")
138+
parts.append("```text")
139+
parts.append(project_tree(root))
140+
parts.append("```")
141+
if include_git:
142+
parts.append("## Git status")
143+
parts.append("```text")
144+
parts.append(git_status(root))
145+
parts.append("```")
146+
return "\n".join(parts) + "\n"
147+
148+
149+
def build_context_pack(store: Store, task_id: str) -> Path:
150+
store.require()
151+
task = store.get_task(task_id)
152+
config = store.read_config()
153+
max_chars = int(config.get("context", {}).get("max_file_chars", 12000))
154+
include_tree = bool(config.get("context", {}).get("include_tree", True))
155+
include_git = bool(config.get("context", {}).get("include_git_status", True))
156+
157+
out_dir = store.base / "context" / task.id
158+
out_dir.mkdir(parents=True, exist_ok=True)
159+
160+
files = {
161+
"task-brief.md": build_task_brief(task, config.get("project_name", store.root.name), config.get("description", "")),
162+
"agent-instructions.md": build_agent_instructions(task),
163+
"relevant-files.md": build_relevant_files(store.root, task, max_chars),
164+
"commands.md": build_commands(store.root, config.get("commands", {})),
165+
"environment.md": build_environment(store.root, include_tree, include_git),
166+
}
167+
for name, content in files.items():
168+
(out_dir / name).write_text(content, encoding="utf-8")
169+
170+
combined = ["# AgentFlowDesk Context Pack", ""]
171+
for name in ["task-brief.md", "agent-instructions.md", "commands.md", "environment.md", "relevant-files.md"]:
172+
combined.append(f"\n---\n\n<!-- {name} -->\n")
173+
combined.append((out_dir / name).read_text(encoding="utf-8"))
174+
(out_dir / "context-pack.md").write_text("\n".join(combined), encoding="utf-8")
175+
return out_dir

0 commit comments

Comments
 (0)