codee-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codee/.gitignore +2 -0
- codee/__init__.py +0 -0
- codee/admin.py +1672 -0
- codee/admin_api.py +61 -0
- codee/admin_cli.py +86 -0
- codee/admin_service.py +1005 -0
- codee/executor.py +381 -0
- codee/init_cli.py +82 -0
- codee/lib/__init__.py +0 -0
- codee/lib/cron_describe.py +33 -0
- codee/lib/runs_db.py +195 -0
- codee/lib/test_runs_db.py +199 -0
- codee/lib/test_trigger_cron_skills.py +485 -0
- codee/lib/test_trigger_issue_skills.py +93 -0
- codee/lib/trigger_aws_sqs_skills.py +224 -0
- codee/lib/trigger_cron_skills.py +364 -0
- codee/lib/trigger_email_skills.py +225 -0
- codee/lib/trigger_issue_skills.py +107 -0
- codee/mail_server.py +45 -0
- codee/start_cli.py +98 -0
- codee/templates/AGENTS.md +42 -0
- codee/templates/CLAUDE.md +1 -0
- codee/templates/skills/aws-sqs-alarm-response/SKILL.md +23 -0
- codee/templates/skills/cron-research-5xx-errors/SKILL.md +17 -0
- codee/templates/skills/story-code-reviewer/SKILL.md +29 -0
- codee/templates/skills/story-developer/SKILL.md +26 -0
- codee/templates/skills/story-planner/SKILL.md +35 -0
- codee/templates/skills/story-planner/assets/readme-template.md +43 -0
- codee/templates/skills/story-qa/SKILL.md +28 -0
- codee/templates/skills/task-developer/SKILL.md +25 -0
- codee/templates/skills/task-qa/SKILL.md +26 -0
- codee/test_admin_api.py +64 -0
- codee/test_admin_cli.py +63 -0
- codee/test_admin_service.py +897 -0
- codee/test_executor.py +190 -0
- codee/test_init_cli.py +131 -0
- codee/test_memory_index.py +31 -0
- codee/test_start_cli.py +164 -0
- codee/workflow_graph.py +83 -0
- codee_admin/__init__.py +1 -0
- codee_admin/codee_admin.py +4 -0
- codee_agent-0.1.0.dist-info/METADATA +66 -0
- codee_agent-0.1.0.dist-info/RECORD +69 -0
- codee_agent-0.1.0.dist-info/WHEEL +4 -0
- codee_agent-0.1.0.dist-info/entry_points.txt +6 -0
- codee_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- codee_agent_abstract/__init__.py +0 -0
- codee_agent_abstract/provider.py +56 -0
- codee_agent_claude_code/__init__.py +0 -0
- codee_agent_claude_code/provider.py +90 -0
- codee_agent_github_copilot/__init__.py +0 -0
- codee_agent_github_copilot/provider.py +253 -0
- codee_agent_github_copilot/test.py +176 -0
- codee_database/__init__.py +0 -0
- codee_database/database.py +13 -0
- codee_database/oauth_tokens.py +148 -0
- codee_main_context/__init__.py +0 -0
- codee_main_context/context.py +127 -0
- codee_main_context/logging.py +111 -0
- codee_main_context/test_logging.py +90 -0
- codee_tasks_abstract/__init__.py +0 -0
- codee_tasks_abstract/provider.py +58 -0
- codee_tasks_azure_devops/__init__.py +0 -0
- codee_tasks_azure_devops/oauth.py +346 -0
- codee_tasks_azure_devops/provider.py +207 -0
- codee_tasks_azure_devops/test.py +462 -0
- codee_tasks_jira/__init__.py +0 -0
- codee_tasks_jira/provider.py +162 -0
- codee_tasks_jira/test.py +75 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import email
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from email.message import Message
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from codee_main_context.context import CodeeMainContext
|
|
11
|
+
|
|
12
|
+
from codee.lib import runs_db
|
|
13
|
+
from codee.lib.trigger_aws_sqs_skills import _parse_skill_file, _skill_key
|
|
14
|
+
from codee_main_context.context import project_root, skills_dir as default_skills_dir
|
|
15
|
+
|
|
16
|
+
REPO_ROOT = project_root()
|
|
17
|
+
SKILLS_DIR = default_skills_dir(REPO_ROOT)
|
|
18
|
+
EMAILS_DIR = REPO_ROOT / "temp-emails"
|
|
19
|
+
|
|
20
|
+
# Max emails handled per cron tick (across all email-triggered skills).
|
|
21
|
+
MAX_EMAILS_PER_TICK = 3
|
|
22
|
+
|
|
23
|
+
# Comma-separated sender domains. When unset, email-triggered skills remain disabled.
|
|
24
|
+
ALLOWED_SENDER_DOMAINS = tuple(
|
|
25
|
+
domain.strip().lower()
|
|
26
|
+
for domain in os.environ.get("CODEE_ALLOWED_SENDER_DOMAINS", "").split(",")
|
|
27
|
+
if domain.strip()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
RunClaude = Callable[[str, str, str], str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class EmailTriggeredSkill:
|
|
35
|
+
key: str
|
|
36
|
+
name: str
|
|
37
|
+
path: Path
|
|
38
|
+
address: str
|
|
39
|
+
body: str
|
|
40
|
+
model: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def trigger_email_skills(
|
|
44
|
+
run_claude: RunClaude,
|
|
45
|
+
*,
|
|
46
|
+
skills_dir: Path = SKILLS_DIR,
|
|
47
|
+
emails_dir: Path = EMAILS_DIR,
|
|
48
|
+
main_context: CodeeMainContext
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Route up to MAX_EMAILS_PER_TICK queued emails to skills by recipient address."""
|
|
51
|
+
skills_by_address = find_email_triggered_skills(skills_dir)
|
|
52
|
+
if not skills_by_address:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
emails_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
files = sorted(p for p in emails_dir.glob("*.eml") if p.is_file())
|
|
57
|
+
if not files:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
for path in files[:MAX_EMAILS_PER_TICK]:
|
|
61
|
+
try:
|
|
62
|
+
message = email.message_from_bytes(path.read_bytes())
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
print(f"[email_skills] Failed to read {path}: {exc}")
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if not _sender_allowed(message):
|
|
68
|
+
print(
|
|
69
|
+
f"[email_skills] Sender of {path.name} not in allowed domains; dropping.")
|
|
70
|
+
path.unlink(missing_ok=True)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
skill = _match_skill(message, skills_by_address)
|
|
74
|
+
if skill is None:
|
|
75
|
+
print(
|
|
76
|
+
f"[email_skills] No skill matches recipients of {path.name}; dropping.")
|
|
77
|
+
path.unlink(missing_ok=True)
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
print(
|
|
81
|
+
f"[email_skills] Running {skill.name} for email {path.name} -> {skill.address}")
|
|
82
|
+
session_id = str(uuid.uuid4())
|
|
83
|
+
prompt = render_email_prompt(skill.body, message)
|
|
84
|
+
try:
|
|
85
|
+
response = run_claude(prompt, session_id, skill.model)
|
|
86
|
+
print(
|
|
87
|
+
f"[email_skills] Claude response for {skill.name} ({len(response)} chars)")
|
|
88
|
+
path.unlink(missing_ok=True)
|
|
89
|
+
runs_db.record_run(skill.name, "email", session_id,
|
|
90
|
+
"succeeded", message=prompt,
|
|
91
|
+
main_context=main_context)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
print(
|
|
94
|
+
f"[email_skills] Failed to run {skill.name} for {path.name}: {exc}")
|
|
95
|
+
runs_db.record_run(skill.name, "email", session_id, "failed",
|
|
96
|
+
error=str(exc)[:500], message=prompt,
|
|
97
|
+
main_context=main_context)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def find_email_triggered_skills(skills_dir: Path = SKILLS_DIR) -> dict[str, EmailTriggeredSkill]:
|
|
101
|
+
"""Map normalized recipient address -> skill. Duplicate addresses are skipped."""
|
|
102
|
+
skills_by_address: dict[str, EmailTriggeredSkill] = {}
|
|
103
|
+
for path in sorted(skills_dir.glob("*/SKILL.md")):
|
|
104
|
+
try:
|
|
105
|
+
metadata, body = _parse_skill_file(path.read_text())
|
|
106
|
+
except OSError as exc:
|
|
107
|
+
print(f"[email_skills] Failed to read {path}: {exc}")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if metadata.get("x-codee-trigger", "").strip().lower() != "email":
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
name = metadata.get(
|
|
114
|
+
"name", path.parent.name).strip() or path.parent.name
|
|
115
|
+
if metadata.get("disable-model-invocation", "").strip().lower() != "true":
|
|
116
|
+
print(
|
|
117
|
+
f"[email_skills] ERROR: {path} declares x-codee-trigger: email but is missing "
|
|
118
|
+
"disable-model-invocation: true; skipping."
|
|
119
|
+
)
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
address = metadata.get("x-codee-email-address", "").strip().lower()
|
|
123
|
+
if not address:
|
|
124
|
+
print(
|
|
125
|
+
f"[email_skills] ERROR: {path} declares x-codee-trigger: email but is missing "
|
|
126
|
+
"x-codee-email-address; skipping."
|
|
127
|
+
)
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if address in skills_by_address:
|
|
131
|
+
print(
|
|
132
|
+
f"[email_skills] WARNING: duplicate x-codee-email-address {address!r} in {path}; "
|
|
133
|
+
f"already claimed by {skills_by_address[address].path}. Skipping."
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
skills_by_address[address] = EmailTriggeredSkill(
|
|
138
|
+
key=_skill_key(path),
|
|
139
|
+
name=name,
|
|
140
|
+
path=path,
|
|
141
|
+
address=address,
|
|
142
|
+
body=body.strip(),
|
|
143
|
+
model=metadata.get("model", "").strip(),
|
|
144
|
+
)
|
|
145
|
+
return skills_by_address
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _sender_allowed(message: Message) -> bool:
|
|
149
|
+
_, addr = email.utils.parseaddr(message.get("From", ""))
|
|
150
|
+
domain = addr.rsplit("@", 1)[-1].lower() if "@" in addr else ""
|
|
151
|
+
return domain in ALLOWED_SENDER_DOMAINS
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _match_skill(
|
|
155
|
+
message: Message, skills_by_address: dict[str, EmailTriggeredSkill]
|
|
156
|
+
) -> EmailTriggeredSkill | None:
|
|
157
|
+
for address in _recipients(message):
|
|
158
|
+
skill = skills_by_address.get(address)
|
|
159
|
+
if skill is not None:
|
|
160
|
+
return skill
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _recipients(message: Message) -> list[str]:
|
|
165
|
+
"""All recipient addresses, normalized to lowercase. Envelope header wins."""
|
|
166
|
+
headers = ["X-Codee-Rcpt", "Delivered-To", "To", "Cc", "Bcc"]
|
|
167
|
+
raw = ", ".join(v for h in headers for v in message.get_all(h, []))
|
|
168
|
+
return [addr.lower() for _, addr in email.utils.getaddresses([raw]) if addr]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def render_email_prompt(body: str, message: Message) -> str:
|
|
172
|
+
content = _format_email(message)
|
|
173
|
+
if "{CONTENT}" in body:
|
|
174
|
+
return body.replace("{CONTENT}", content)
|
|
175
|
+
return f"{body.rstrip()}\n\n{content}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _format_email(message: Message) -> str:
|
|
179
|
+
headers = [f"{key}: {value}" for key, value in message.items()]
|
|
180
|
+
parts = [*headers, "", _body_text(message)]
|
|
181
|
+
attachments = _save_attachments(message)
|
|
182
|
+
if attachments:
|
|
183
|
+
parts.append("")
|
|
184
|
+
parts.append("Attachments (saved to disk):")
|
|
185
|
+
parts.extend(f"- {name}: {path}" for name, path in attachments)
|
|
186
|
+
return "\n".join(parts).strip()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _save_attachments(message: Message) -> list[tuple[str, Path]]:
|
|
190
|
+
"""Write every attachment to a temp dir; return (filename, path) references."""
|
|
191
|
+
saved: list[tuple[str, Path]] = []
|
|
192
|
+
dest: Path | None = None
|
|
193
|
+
for part in message.walk():
|
|
194
|
+
filename = part.get_filename()
|
|
195
|
+
if not filename or part.get_content_disposition() == "inline":
|
|
196
|
+
continue
|
|
197
|
+
payload = part.get_payload(decode=True)
|
|
198
|
+
if payload is None:
|
|
199
|
+
continue
|
|
200
|
+
if dest is None:
|
|
201
|
+
dest = Path(tempfile.mkdtemp(prefix="email-attach-"))
|
|
202
|
+
safe = Path(filename).name # strip any path components
|
|
203
|
+
path = dest / safe
|
|
204
|
+
path.write_bytes(payload)
|
|
205
|
+
saved.append((safe, path))
|
|
206
|
+
return saved
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _body_text(message: Message) -> str:
|
|
210
|
+
try:
|
|
211
|
+
part = message.get_body(preferencelist=("plain", "html"))
|
|
212
|
+
except (AttributeError, Exception): # noqa: BLE001 - non-EmailMessage fallback
|
|
213
|
+
part = None
|
|
214
|
+
if part is not None:
|
|
215
|
+
return part.get_content().strip()
|
|
216
|
+
|
|
217
|
+
if message.is_multipart():
|
|
218
|
+
for sub in message.walk():
|
|
219
|
+
if sub.get_content_type() == "text/plain":
|
|
220
|
+
return sub.get_payload(decode=True).decode(sub.get_content_charset() or "utf-8", "replace").strip()
|
|
221
|
+
return ""
|
|
222
|
+
payload = message.get_payload(decode=True)
|
|
223
|
+
if payload is None:
|
|
224
|
+
return message.get_payload()
|
|
225
|
+
return payload.decode(message.get_content_charset() or "utf-8", "replace").strip()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
from codee_main_context.context import project_root, skills_dir as default_skills_dir
|
|
8
|
+
|
|
9
|
+
REPO_ROOT = project_root()
|
|
10
|
+
SKILLS_DIR = default_skills_dir(REPO_ROOT)
|
|
11
|
+
ISSUE_TYPES = ("story", "task")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class IssueTriggeredSkill:
|
|
16
|
+
name: str
|
|
17
|
+
slug: str
|
|
18
|
+
path: Path
|
|
19
|
+
statuses: tuple[str, ...]
|
|
20
|
+
issue_type: str
|
|
21
|
+
model: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_issue_triggered_skills(
|
|
25
|
+
skills_dir: Path = SKILLS_DIR,
|
|
26
|
+
) -> list[IssueTriggeredSkill]:
|
|
27
|
+
"""Load valid issue-triggered skills from skill frontmatter."""
|
|
28
|
+
skills: list[IssueTriggeredSkill] = []
|
|
29
|
+
for path in sorted(skills_dir.glob("*/SKILL.md")):
|
|
30
|
+
try:
|
|
31
|
+
metadata = _parse_frontmatter(path.read_text())
|
|
32
|
+
except (OSError, yaml.YAMLError) as exc:
|
|
33
|
+
print(f"[issue_skills] Failed to read {path}: {exc}")
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
if str(metadata.get("x-codee-trigger", "")).strip().lower() != "issue":
|
|
37
|
+
continue
|
|
38
|
+
if metadata.get("disable-model-invocation") is not True:
|
|
39
|
+
print(
|
|
40
|
+
f"[issue_skills] ERROR: {path} declares x-codee-trigger: issue but is "
|
|
41
|
+
"missing disable-model-invocation: true; skipping."
|
|
42
|
+
)
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
statuses = _status_values(metadata.get("x-codee-issue-status"))
|
|
46
|
+
if not statuses:
|
|
47
|
+
print(
|
|
48
|
+
f"[issue_skills] ERROR: {path} declares x-codee-trigger: issue but is "
|
|
49
|
+
"missing x-codee-issue-status; skipping."
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
raw_issue_type = metadata.get("x-codee-issue-type")
|
|
54
|
+
issue_type = str(raw_issue_type).strip().lower()
|
|
55
|
+
if not isinstance(raw_issue_type, str) or issue_type not in ISSUE_TYPES:
|
|
56
|
+
print(
|
|
57
|
+
f"[issue_skills] ERROR: {path} declares x-codee-trigger: issue but "
|
|
58
|
+
"x-codee-issue-type must be story or task; skipping."
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
skills.append(IssueTriggeredSkill(
|
|
62
|
+
name=str(metadata.get("name", path.parent.name)
|
|
63
|
+
).strip() or path.parent.name,
|
|
64
|
+
slug=path.parent.name,
|
|
65
|
+
path=path,
|
|
66
|
+
statuses=statuses,
|
|
67
|
+
issue_type=issue_type,
|
|
68
|
+
model=str(metadata.get("model", "")).strip(),
|
|
69
|
+
))
|
|
70
|
+
return skills
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def issue_statuses(skills: list[IssueTriggeredSkill]) -> list[str]:
|
|
74
|
+
"""Return unique configured statuses while preserving declaration order."""
|
|
75
|
+
return list(dict.fromkeys(status for skill in skills for status in skill.statuses))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def match_issue_skill(
|
|
79
|
+
skills: list[IssueTriggeredSkill], status: str, issue_type: str
|
|
80
|
+
) -> IssueTriggeredSkill | None:
|
|
81
|
+
"""Find a skill matching both status and issue type."""
|
|
82
|
+
normalized_status = status.casefold()
|
|
83
|
+
normalized_issue_type = issue_type.casefold()
|
|
84
|
+
|
|
85
|
+
return next((
|
|
86
|
+
skill for skill in skills
|
|
87
|
+
if skill.issue_type.casefold() == normalized_issue_type
|
|
88
|
+
and any(value.casefold() == normalized_status for value in skill.statuses)
|
|
89
|
+
), None)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _parse_frontmatter(contents: str) -> dict[str, Any]:
|
|
93
|
+
if not contents.startswith("---"):
|
|
94
|
+
return {}
|
|
95
|
+
parts = contents.split("---", 2)
|
|
96
|
+
if len(parts) < 3:
|
|
97
|
+
return {}
|
|
98
|
+
parsed = yaml.safe_load(parts[1]) or {}
|
|
99
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _status_values(value: Any) -> tuple[str, ...]:
|
|
103
|
+
values = value if isinstance(value, list) else [value]
|
|
104
|
+
return tuple(
|
|
105
|
+
status for item in values
|
|
106
|
+
if item is not None and (status := str(item).strip())
|
|
107
|
+
)
|
codee/mail_server.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""SMTP server that drops each incoming email into temp-emails/ as an .eml file.
|
|
2
|
+
|
|
3
|
+
The cron tick (trigger_email_skills) picks them up and routes them to skills by
|
|
4
|
+
recipient address. Listens on a non-privileged port; map port 25 -> MAIL_PORT at
|
|
5
|
+
the infra layer (iptables/load balancer), not here.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import uuid
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from aiosmtpd.controller import Controller
|
|
13
|
+
|
|
14
|
+
EMAILS_DIR = Path(__file__).parent.parent / "temp-emails"
|
|
15
|
+
MAIL_HOST = os.environ.get("MAIL_HOST", "0.0.0.0")
|
|
16
|
+
MAIL_PORT = int(os.environ.get("MAIL_PORT", "2525"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FileDropHandler:
|
|
20
|
+
async def handle_DATA(self, server, session, envelope):
|
|
21
|
+
EMAILS_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
# Prepend envelope recipients so the router matches on the real RCPT TO,
|
|
23
|
+
# not just the visible To/Cc headers.
|
|
24
|
+
rcpt = ", ".join(envelope.rcpt_tos)
|
|
25
|
+
content = f"X-Codee-Rcpt: {rcpt}\r\n".encode() + envelope.content
|
|
26
|
+
# uuid keeps names unique within a tick; lexical sort ~ arrival order is
|
|
27
|
+
# good enough for a 3-per-tick drain. ponytail: no timestamp needed.
|
|
28
|
+
path = EMAILS_DIR / f"{uuid.uuid4().hex}.eml"
|
|
29
|
+
path.write_bytes(content)
|
|
30
|
+
print(f"[mail_server] Stored email -> {path.name} for {rcpt}")
|
|
31
|
+
return "250 Message accepted for delivery"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
controller = Controller(FileDropHandler(), hostname=MAIL_HOST, port=MAIL_PORT)
|
|
36
|
+
controller.start()
|
|
37
|
+
print(f"[mail_server] Listening on {MAIL_HOST}:{MAIL_PORT}, dropping to {EMAILS_DIR}")
|
|
38
|
+
try:
|
|
39
|
+
asyncio.get_event_loop().run_forever()
|
|
40
|
+
except KeyboardInterrupt:
|
|
41
|
+
controller.stop()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
codee/start_cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from codee_main_context.logging import (
|
|
8
|
+
configure_logging, enable_debug, get_logger)
|
|
9
|
+
|
|
10
|
+
from codee.init_cli import (
|
|
11
|
+
CONFLICT_PATHS, ensure_working_directories, main as init_main)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SHUTDOWN_TIMEOUT = 5
|
|
15
|
+
|
|
16
|
+
log = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_arguments(argv: list[str]) -> tuple[argparse.Namespace, list[str]]:
|
|
20
|
+
"""Pull out codee-start's own flags; everything else goes to the admin UI."""
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="codee-start",
|
|
23
|
+
epilog="Any other argument (--port, reflex flags) goes to the admin UI.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-d", "--debug",
|
|
27
|
+
action="store_true",
|
|
28
|
+
help="log debug messages from the executor and the admin UI",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--debug-all",
|
|
32
|
+
action="store_true",
|
|
33
|
+
help="--debug plus debug output from third-party libraries",
|
|
34
|
+
)
|
|
35
|
+
return parser.parse_known_args(argv)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ensure_initialized() -> None:
|
|
39
|
+
destination = Path.cwd()
|
|
40
|
+
if any((destination / path).exists() for path in CONFLICT_PATHS):
|
|
41
|
+
log.info("Codee is already initialized; skipping codee-init")
|
|
42
|
+
# codee-init is skipped, but the runtime directories are still needed
|
|
43
|
+
# here: they may predate this feature, or have been cleaned away.
|
|
44
|
+
ensure_working_directories(destination)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
init_main()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _stop_processes(processes: list[subprocess.Popen[bytes]]) -> None:
|
|
51
|
+
running = [process for process in processes if process.poll() is None]
|
|
52
|
+
for process in running:
|
|
53
|
+
process.terminate()
|
|
54
|
+
|
|
55
|
+
for process in running:
|
|
56
|
+
try:
|
|
57
|
+
process.wait(timeout=SHUTDOWN_TIMEOUT)
|
|
58
|
+
except subprocess.TimeoutExpired:
|
|
59
|
+
process.kill()
|
|
60
|
+
process.wait()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
arguments, admin_arguments = _parse_arguments(sys.argv[1:])
|
|
65
|
+
if arguments.debug or arguments.debug_all:
|
|
66
|
+
# Exports CODEE_DEBUG, which the subprocesses below inherit.
|
|
67
|
+
enable_debug(verbose=arguments.debug_all)
|
|
68
|
+
configure_logging()
|
|
69
|
+
|
|
70
|
+
_ensure_initialized()
|
|
71
|
+
|
|
72
|
+
commands = [
|
|
73
|
+
[sys.executable, "-m", "codee.executor"],
|
|
74
|
+
[sys.executable, "-m", "codee.admin_cli", *admin_arguments],
|
|
75
|
+
]
|
|
76
|
+
processes: list[subprocess.Popen[bytes]] = []
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
for command in commands:
|
|
80
|
+
log.debug("starting %s", " ".join(command))
|
|
81
|
+
processes.append(subprocess.Popen(command))
|
|
82
|
+
|
|
83
|
+
while True:
|
|
84
|
+
for process in processes:
|
|
85
|
+
return_code = process.poll()
|
|
86
|
+
if return_code is not None:
|
|
87
|
+
log.debug("pid %s exited with %s",
|
|
88
|
+
process.pid, return_code)
|
|
89
|
+
return return_code
|
|
90
|
+
time.sleep(0.2)
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
return 130
|
|
93
|
+
finally:
|
|
94
|
+
_stop_processes(processes)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Basic
|
|
2
|
+
|
|
3
|
+
You are Codee, an employee.
|
|
4
|
+
|
|
5
|
+
# Repository Guidelines
|
|
6
|
+
|
|
7
|
+
Always read nested `AGENTS.md` and `CLAUDE.md` files in the projects you work with.
|
|
8
|
+
|
|
9
|
+
## Project Skills
|
|
10
|
+
|
|
11
|
+
When working with a project, scan its `.claude/skills` directory for relevant skill files. Read and follow applicable skills before starting the task.
|
|
12
|
+
|
|
13
|
+
- Load debugging guidance for bugs, regressions, failing tests or pipelines, incidents, pasted errors, and reported broken behavior.
|
|
14
|
+
- Load code-review guidance for branch, implementation, and change reviews.
|
|
15
|
+
- Load frontend guidance before UI, UX, styling, browser-validation, user-visible copy, or localization work.
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
- Keep changes focused on the requested task.
|
|
20
|
+
- Build and test changed projects before finishing.
|
|
21
|
+
- Do not use unbounded polling loops for CI, deployments, or HTTP readiness. Use a counted timeout and report the final state.
|
|
22
|
+
|
|
23
|
+
### Repositores
|
|
24
|
+
|
|
25
|
+
Repositories are located in `repositories` directory. Each repository contains .bare folder with bare git repo.
|
|
26
|
+
|
|
27
|
+
### Worktrees
|
|
28
|
+
|
|
29
|
+
Each branch is its own worktree dir under `repositories/<repo>/` — never `git checkout` in place. From `repositories/<repo>` (the `.git` file points git at `.bare`, so no `-C` needed):
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
git fetch origin # get latest
|
|
33
|
+
git worktree add -b <branch> <branch> origin/master # new branch off master
|
|
34
|
+
git worktree add <branch> # check out existing branch
|
|
35
|
+
cd <branch> # this is your working dir; npm install here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`git worktree list` to see them, `git worktree remove <branch>` when done. `git` inside a worktree dir works normally (add/commit/push).
|
|
39
|
+
|
|
40
|
+
## Temporary Files
|
|
41
|
+
|
|
42
|
+
Save screenshots, videos, generated configuration, and other temporary artifacts under `./temp` instead of the repository root.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aws-sqs-alarm-response
|
|
3
|
+
description: Investigate a production alarm and record the findings in the Issue Tracker.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
x-codee-trigger: aws-sqs
|
|
6
|
+
x-codee-aws-sqs-queue: codee-alarms
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Alarm Response
|
|
10
|
+
|
|
11
|
+
An alarm was triggered with this content:
|
|
12
|
+
|
|
13
|
+
{CONTENT}
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Determine which service or user flow is affected.
|
|
18
|
+
2. Gather relevant logs, metrics, traces, request data, and screenshots.
|
|
19
|
+
3. Identify the likely cause, impact, and any immediate mitigation.
|
|
20
|
+
4. Search the Issue Tracker for an existing open issue about the same problem.
|
|
21
|
+
5. Update the existing issue, or create a new issue with the evidence, impact, and recommended next steps.
|
|
22
|
+
|
|
23
|
+
For a canary failure, include screenshots from the failed step and inspect the available network trace. If the alarm reports recovery, add that information to the related issue. Ignore confirmed visual false positives.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cron-research-5xx-errors
|
|
3
|
+
description: Investigate frequent 5xx errors from the last 24 hours and report the findings.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
x-codee-trigger: cron
|
|
6
|
+
x-codee-cron: 0 0 * * 2-6
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# 5xx Error Review
|
|
10
|
+
|
|
11
|
+
1. Review available gateway, ingress, and application logs for the last 24 hours.
|
|
12
|
+
2. Group 5xx responses by root cause or failing endpoint.
|
|
13
|
+
3. Investigate the three most frequent groups.
|
|
14
|
+
4. For each group, record frequency, impact, evidence, likely cause, and recommended action.
|
|
15
|
+
5. Create one Issue Tracker report, or update an existing open issue when it covers the same errors.
|
|
16
|
+
|
|
17
|
+
Do not make speculative code changes. Clearly separate confirmed findings from hypotheses.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: story-code-reviewer
|
|
3
|
+
description: Review a merge request for one subtask in an Issue Tracker story.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
x-codee-trigger: issue
|
|
6
|
+
x-codee-issue-status: ['[AI] CR Needed']
|
|
7
|
+
x-codee-issue-type: story
|
|
8
|
+
argument-hint: <STORY_ID>
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Story Code Reviewer
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. Read the story, acceptance criteria, attachments, comments, and linked subtasks in the Issue Tracker.
|
|
16
|
+
2. Read `story-spec/{STORY_ID}/README.md` when it exists.
|
|
17
|
+
3. Select one subtask with a merge request awaiting review.
|
|
18
|
+
4. Read the project instructions, complete diff, affected files, relevant callers, tests, pipeline results, and unresolved review comments.
|
|
19
|
+
5. Review security, correctness, requirement coverage, performance, tests, error handling, and maintainability.
|
|
20
|
+
6. Post line comments for specific defects and a findings-first merge request review.
|
|
21
|
+
7. Add a concise Issue Tracker comment with the verdict, blocking findings, and verification performed.
|
|
22
|
+
|
|
23
|
+
## Guidelines
|
|
24
|
+
|
|
25
|
+
- Review one subtask per invocation.
|
|
26
|
+
- Distinguish blocking defects from optional suggestions.
|
|
27
|
+
- Do not approve changes with unresolved blocking findings.
|
|
28
|
+
- Do not approve a failing pipeline unless the failure is clearly unrelated.
|
|
29
|
+
- Make feedback actionable by naming the observed behavior, expected behavior, and affected location.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: story-developer
|
|
3
|
+
description: Implement one subtask from an Issue Tracker story and submit a merge request.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
x-codee-trigger: issue
|
|
6
|
+
x-codee-issue-status: ['[AI] Ready for development', '[AI] In Progress']
|
|
7
|
+
x-codee-issue-type: story
|
|
8
|
+
argument-hint: <STORY_ID>
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Story Developer
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. Read the story, acceptance criteria, attachments, comments, and subtasks in the Issue Tracker.
|
|
16
|
+
2. Read `story-spec/{STORY_ID}/README.md` and the affected projects' `CLAUDE.md`, `AGENTS.md`, and relevant local skills.
|
|
17
|
+
3. Select one open subtask and inspect any related branches, merge requests, and review comments.
|
|
18
|
+
4. For bugs and regressions, reproduce the problem and identify the root cause before editing code.
|
|
19
|
+
5. Create or reuse a dedicated worktree and branch according to repository conventions.
|
|
20
|
+
6. Install dependencies, implement the smallest complete change, and update the story specification when useful.
|
|
21
|
+
7. Run focused tests, the project build, and any required browser checks.
|
|
22
|
+
8. Review the complete diff for correctness, security, regressions, and missing tests.
|
|
23
|
+
9. Commit, push, and create or update the merge request. Do not merge it unless explicitly requested.
|
|
24
|
+
10. Add a concise Issue Tracker comment describing the change, validation, merge request, and any remaining risk.
|
|
25
|
+
|
|
26
|
+
For UI work, include screenshots or video evidence when appropriate.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: story-planner
|
|
3
|
+
description: Decompose an Issue Tracker story into actionable subtasks and supporting documentation.
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
x-codee-trigger: issue
|
|
6
|
+
x-codee-issue-status: ['[AI] Decomposition Needed']
|
|
7
|
+
x-codee-issue-type: story
|
|
8
|
+
argument-hint: <STORY_ID>
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Story Planner
|
|
12
|
+
|
|
13
|
+
Decompose an Issue Tracker story into actionable subtasks with supporting documentation.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. Read the story, acceptance criteria, attachments, comments, and linked issues in the Issue Tracker.
|
|
18
|
+
2. Inspect relevant project instructions and source code to understand the current behavior.
|
|
19
|
+
3. Ask focused questions when requirements, constraints, or expected behavior are unclear. Record the questions in the Issue Tracker and stop until they are answered.
|
|
20
|
+
4. Create a dependency-ordered plan of small, independently implementable subtasks.
|
|
21
|
+
5. Give each subtask a clear title, technical description, acceptance criteria, dependencies, and estimate when useful.
|
|
22
|
+
6. Ensure the subtasks cover every story acceptance criterion, including testing and monitoring work where needed.
|
|
23
|
+
7. Create or update the subtasks in the Issue Tracker using its supported rich-text format.
|
|
24
|
+
8. Write the specification to `story-spec/{STORY_ID}/README.md` using [assets/readme-template.md](assets/readme-template.md). Add `architecture.md` only when architecture or data flow needs explanation.
|
|
25
|
+
9. Post a concise Issue Tracker comment summarizing the plan and linking the subtasks and specification.
|
|
26
|
+
|
|
27
|
+
## Feedback Rounds
|
|
28
|
+
|
|
29
|
+
When a plan already exists, re-read new comments and edit the existing subtasks and specification. Create new subtasks only for newly identified work, and avoid duplicates.
|
|
30
|
+
|
|
31
|
+
## Guidelines
|
|
32
|
+
|
|
33
|
+
- Prefer subtasks that can be implemented and reviewed independently.
|
|
34
|
+
- Order subtasks by dependency.
|
|
35
|
+
- Keep technical details concrete and acceptance criteria verifiable.
|