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.
Files changed (69) hide show
  1. codee/.gitignore +2 -0
  2. codee/__init__.py +0 -0
  3. codee/admin.py +1672 -0
  4. codee/admin_api.py +61 -0
  5. codee/admin_cli.py +86 -0
  6. codee/admin_service.py +1005 -0
  7. codee/executor.py +381 -0
  8. codee/init_cli.py +82 -0
  9. codee/lib/__init__.py +0 -0
  10. codee/lib/cron_describe.py +33 -0
  11. codee/lib/runs_db.py +195 -0
  12. codee/lib/test_runs_db.py +199 -0
  13. codee/lib/test_trigger_cron_skills.py +485 -0
  14. codee/lib/test_trigger_issue_skills.py +93 -0
  15. codee/lib/trigger_aws_sqs_skills.py +224 -0
  16. codee/lib/trigger_cron_skills.py +364 -0
  17. codee/lib/trigger_email_skills.py +225 -0
  18. codee/lib/trigger_issue_skills.py +107 -0
  19. codee/mail_server.py +45 -0
  20. codee/start_cli.py +98 -0
  21. codee/templates/AGENTS.md +42 -0
  22. codee/templates/CLAUDE.md +1 -0
  23. codee/templates/skills/aws-sqs-alarm-response/SKILL.md +23 -0
  24. codee/templates/skills/cron-research-5xx-errors/SKILL.md +17 -0
  25. codee/templates/skills/story-code-reviewer/SKILL.md +29 -0
  26. codee/templates/skills/story-developer/SKILL.md +26 -0
  27. codee/templates/skills/story-planner/SKILL.md +35 -0
  28. codee/templates/skills/story-planner/assets/readme-template.md +43 -0
  29. codee/templates/skills/story-qa/SKILL.md +28 -0
  30. codee/templates/skills/task-developer/SKILL.md +25 -0
  31. codee/templates/skills/task-qa/SKILL.md +26 -0
  32. codee/test_admin_api.py +64 -0
  33. codee/test_admin_cli.py +63 -0
  34. codee/test_admin_service.py +897 -0
  35. codee/test_executor.py +190 -0
  36. codee/test_init_cli.py +131 -0
  37. codee/test_memory_index.py +31 -0
  38. codee/test_start_cli.py +164 -0
  39. codee/workflow_graph.py +83 -0
  40. codee_admin/__init__.py +1 -0
  41. codee_admin/codee_admin.py +4 -0
  42. codee_agent-0.1.0.dist-info/METADATA +66 -0
  43. codee_agent-0.1.0.dist-info/RECORD +69 -0
  44. codee_agent-0.1.0.dist-info/WHEEL +4 -0
  45. codee_agent-0.1.0.dist-info/entry_points.txt +6 -0
  46. codee_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  47. codee_agent_abstract/__init__.py +0 -0
  48. codee_agent_abstract/provider.py +56 -0
  49. codee_agent_claude_code/__init__.py +0 -0
  50. codee_agent_claude_code/provider.py +90 -0
  51. codee_agent_github_copilot/__init__.py +0 -0
  52. codee_agent_github_copilot/provider.py +253 -0
  53. codee_agent_github_copilot/test.py +176 -0
  54. codee_database/__init__.py +0 -0
  55. codee_database/database.py +13 -0
  56. codee_database/oauth_tokens.py +148 -0
  57. codee_main_context/__init__.py +0 -0
  58. codee_main_context/context.py +127 -0
  59. codee_main_context/logging.py +111 -0
  60. codee_main_context/test_logging.py +90 -0
  61. codee_tasks_abstract/__init__.py +0 -0
  62. codee_tasks_abstract/provider.py +58 -0
  63. codee_tasks_azure_devops/__init__.py +0 -0
  64. codee_tasks_azure_devops/oauth.py +346 -0
  65. codee_tasks_azure_devops/provider.py +207 -0
  66. codee_tasks_azure_devops/test.py +462 -0
  67. codee_tasks_jira/__init__.py +0 -0
  68. codee_tasks_jira/provider.py +162 -0
  69. 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.