codee-agent 0.7.0__tar.gz → 0.7.2__tar.gz
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_agent-0.7.0 → codee_agent-0.7.2}/PKG-INFO +1 -1
- {codee_agent-0.7.0 → codee_agent-0.7.2}/pyproject.toml +1 -1
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/admin.py +23 -3
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/admin_service.py +45 -6
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/executor.py +8 -0
- codee_agent-0.7.2/src/codee/lib/runtime_control.py +21 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_admin_service.py +100 -44
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_executor.py +21 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/LICENSE +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/README.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/.gitignore +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/admin_api.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/admin_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/agent_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/coding_agents.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/init_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/claude_key_rotation.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/cron_describe.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/mcp_config.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/runs_db.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/test_claude_key_rotation.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/test_mcp_config.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/test_runs_db.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/test_trigger_cron_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/test_trigger_issue_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/trigger_aws_sqs_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/trigger_cron_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/trigger_email_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/lib/trigger_issue_skills.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/mail_server.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/setup_wizard.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/start_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/tasks_providers.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/AGENTS.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/CLAUDE.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-developer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-planner/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-planner/assets/readme-template.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-qa/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-code-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-developer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-qa/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-security-reviewer/SKILL.md +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_admin_api.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_admin_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_agent_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_coding_agents.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_init_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_memory_index.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_setup_wizard.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/test_start_cli.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/workflow_graph.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_admin/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_admin/codee_admin.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_abstract/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_abstract/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/account.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/credentials.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/oauth.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/test.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_claude_code/usage.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_codex/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_codex/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_codex/test.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_github_copilot/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_github_copilot/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_github_copilot/test.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_opencode/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_opencode/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_agent_opencode/test.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_database/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_database/claude_code_accounts.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_database/database.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_database/oauth_tokens.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_main_context/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_main_context/context.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_main_context/logging.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_main_context/test_context.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_main_context/test_logging.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_abstract/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_abstract/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_azure_devops/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_azure_devops/oauth.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_azure_devops/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_azure_devops/test.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_jira/__init__.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_jira/provider.py +0 -0
- {codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee_tasks_jira/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codee-agent
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: A virtual co-worker that picks up Jira and Azure DevOps tasks and solves them with coding agents.
|
|
5
5
|
Keywords: ai,agent,jira,azure-devops,claude-code,github-copilot,codex,opencode,automation
|
|
6
6
|
Author: Denis Kibalko
|
|
@@ -272,6 +272,7 @@ class RunRecord(BaseModel):
|
|
|
272
272
|
started_at: str
|
|
273
273
|
ended_at: str
|
|
274
274
|
duration_label: str
|
|
275
|
+
relative_age_label: str
|
|
275
276
|
session_id: str
|
|
276
277
|
message: str
|
|
277
278
|
user_message: str
|
|
@@ -318,6 +319,7 @@ class AdminState(rx.State):
|
|
|
318
319
|
adding_repository: bool = False
|
|
319
320
|
|
|
320
321
|
active_jobs: list[ActiveJob] = []
|
|
322
|
+
paused: bool = False
|
|
321
323
|
total_runs: int = 0
|
|
322
324
|
last_24h_runs: int = 0
|
|
323
325
|
hourly_runs: list[dict[str, Any]] = []
|
|
@@ -778,10 +780,14 @@ class AdminState(rx.State):
|
|
|
778
780
|
def _refresh_dashboard(self) -> None:
|
|
779
781
|
dashboard = SERVICE.dashboard()
|
|
780
782
|
self.active_jobs = [_active_job(job) for job in dashboard["active"]]
|
|
783
|
+
self.paused = dashboard["paused"]
|
|
781
784
|
self.total_runs = dashboard["counts"]["total"]
|
|
782
785
|
self.last_24h_runs = dashboard["counts"]["last_24h"]
|
|
783
786
|
self.hourly_runs = dashboard["hourly"]
|
|
784
787
|
|
|
788
|
+
def toggle_pause(self) -> None:
|
|
789
|
+
self.paused = SERVICE.set_paused(not self.paused)
|
|
790
|
+
|
|
785
791
|
@rx.event(background=True)
|
|
786
792
|
async def poll_dashboard(self) -> None:
|
|
787
793
|
"""Keep the dashboard live for as long as it is the page on screen.
|
|
@@ -863,6 +869,7 @@ class AdminState(rx.State):
|
|
|
863
869
|
started_at=run["started_at"],
|
|
864
870
|
ended_at=run["ended_at"],
|
|
865
871
|
duration_label=run["duration_label"],
|
|
872
|
+
relative_age_label=run["relative_age_label"],
|
|
866
873
|
session_id=run.get("session_id") or "",
|
|
867
874
|
message=message,
|
|
868
875
|
user_message=(run.get("user_message") or message).strip(),
|
|
@@ -1976,6 +1983,16 @@ def running_panel() -> rx.Component:
|
|
|
1976
1983
|
font_family=MONO, class_name="codee-breathe",
|
|
1977
1984
|
animation="codee-breathe 2.4s ease-in-out infinite"),
|
|
1978
1985
|
),
|
|
1986
|
+
rx.spacer(),
|
|
1987
|
+
rx.button(
|
|
1988
|
+
rx.cond(AdminState.paused,
|
|
1989
|
+
rx.icon("play", size=15),
|
|
1990
|
+
rx.icon("pause", size=15)),
|
|
1991
|
+
rx.cond(AdminState.paused, "Unpause", "Pause"),
|
|
1992
|
+
variant="outline",
|
|
1993
|
+
color_scheme=rx.cond(AdminState.paused, "green", "gray"),
|
|
1994
|
+
on_click=AdminState.toggle_pause,
|
|
1995
|
+
),
|
|
1979
1996
|
spacing="3",
|
|
1980
1997
|
align="center",
|
|
1981
1998
|
width="100%",
|
|
@@ -2572,9 +2589,12 @@ def run_row(run: RunRecord) -> rx.Component:
|
|
|
2572
2589
|
rx.flex(
|
|
2573
2590
|
rx.vstack(rx.hstack(rx.text(run.skill_name, font_weight="600"),
|
|
2574
2591
|
rx.badge(run.status, color_scheme=rx.cond(run.status == "succeeded", "green", "red"))),
|
|
2575
|
-
rx.
|
|
2576
|
-
|
|
2577
|
-
|
|
2592
|
+
rx.hstack(
|
|
2593
|
+
rx.text(local_datetime(run.ended_at)),
|
|
2594
|
+
rx.text("(", run.relative_age_label, ")"),
|
|
2595
|
+
color=MUTED, font_size="0.8rem",
|
|
2596
|
+
font_family="IBM Plex Mono, monospace",
|
|
2597
|
+
spacing="2"),
|
|
2578
2598
|
rx.text("Duration: ", run.duration_label, color=MUTED,
|
|
2579
2599
|
font_size="0.8rem",
|
|
2580
2600
|
font_family="IBM Plex Mono, monospace"),
|
|
@@ -35,6 +35,7 @@ from codee.lib import runs_db
|
|
|
35
35
|
from codee.lib.claude_key_rotation import ensure_fresh
|
|
36
36
|
from codee.lib.cron_describe import describe_cron
|
|
37
37
|
from codee.lib.mcp_config import find_mcp_server, write_mcp_server
|
|
38
|
+
from codee.lib.runtime_control import is_paused, set_paused
|
|
38
39
|
from codee.lib.trigger_cron_skills import request_force_run
|
|
39
40
|
from codee.lib.trigger_issue_skills import (
|
|
40
41
|
IssueTriggeredSkill,
|
|
@@ -59,6 +60,26 @@ from codee_main_context.context import (
|
|
|
59
60
|
work_item_mappings,
|
|
60
61
|
)
|
|
61
62
|
|
|
63
|
+
|
|
64
|
+
def relative_age_label(value: str, now: datetime | None = None) -> str:
|
|
65
|
+
"""Format an ISO timestamp as a compact minute, hour, or day age."""
|
|
66
|
+
try:
|
|
67
|
+
timestamp = datetime.fromisoformat(value)
|
|
68
|
+
except (TypeError, ValueError):
|
|
69
|
+
return ""
|
|
70
|
+
if timestamp.tzinfo is None:
|
|
71
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
72
|
+
current = now or datetime.now(timezone.utc)
|
|
73
|
+
elapsed = max(int((current - timestamp).total_seconds()), 0)
|
|
74
|
+
if elapsed < 3600:
|
|
75
|
+
amount, unit = max(elapsed // 60, 1), "minute"
|
|
76
|
+
elif elapsed < 86400:
|
|
77
|
+
amount, unit = elapsed // 3600, "hour"
|
|
78
|
+
else:
|
|
79
|
+
amount, unit = elapsed // 86400, "day"
|
|
80
|
+
return f"{amount} {unit}{'' if amount == 1 else 's'} ago"
|
|
81
|
+
|
|
82
|
+
|
|
62
83
|
load_dotenv()
|
|
63
84
|
|
|
64
85
|
MANAGED = {
|
|
@@ -1300,16 +1321,25 @@ class AdminService:
|
|
|
1300
1321
|
f"Generating workflow for work item {issue_type.capitalize()} "
|
|
1301
1322
|
f"from {_count(len(skills), 'issue-trigger skill')}..."
|
|
1302
1323
|
)
|
|
1324
|
+
session_id = str(uuid.uuid4())
|
|
1325
|
+
|
|
1326
|
+
def opened(agent_session_id: str) -> None:
|
|
1327
|
+
nonlocal session_id
|
|
1328
|
+
session_id = agent_session_id
|
|
1329
|
+
|
|
1303
1330
|
validation_error = ""
|
|
1304
1331
|
for attempt in range(2):
|
|
1305
|
-
request = prompt
|
|
1306
1332
|
if validation_error:
|
|
1307
|
-
|
|
1308
|
-
"
|
|
1309
|
-
f"{validation_error}. Return corrected JSON only."
|
|
1333
|
+
response = agent.continue_conversation(
|
|
1334
|
+
"Your previous response was invalid: "
|
|
1335
|
+
f"{validation_error}. Return corrected JSON only.",
|
|
1336
|
+
session_id,
|
|
1337
|
+
agent.best_model(),
|
|
1338
|
+
opened,
|
|
1310
1339
|
)
|
|
1311
|
-
|
|
1312
|
-
|
|
1340
|
+
else:
|
|
1341
|
+
response = agent.run(
|
|
1342
|
+
prompt, session_id, agent.best_model(), opened)
|
|
1313
1343
|
payload_text = _strip_code_fence(response)
|
|
1314
1344
|
try:
|
|
1315
1345
|
payload = json.loads(payload_text)
|
|
@@ -2020,8 +2050,14 @@ class AdminService:
|
|
|
2020
2050
|
"active": active,
|
|
2021
2051
|
"counts": runs_db.counts(main_context=self.context),
|
|
2022
2052
|
"hourly": runs_db.runs_by_hour(main_context=self.context),
|
|
2053
|
+
"paused": is_paused(self.context),
|
|
2023
2054
|
}
|
|
2024
2055
|
|
|
2056
|
+
def set_paused(self, paused: bool) -> bool:
|
|
2057
|
+
"""Allow or prevent future automated agent runs."""
|
|
2058
|
+
set_paused(self.context, paused)
|
|
2059
|
+
return is_paused(self.context)
|
|
2060
|
+
|
|
2025
2061
|
def _task_url(self) -> Callable[[str], str]:
|
|
2026
2062
|
"""How to link a work item key, per the configured tasks provider.
|
|
2027
2063
|
|
|
@@ -2050,6 +2086,7 @@ class AdminService:
|
|
|
2050
2086
|
search: str = "") -> list[dict[str, Any]]:
|
|
2051
2087
|
rows = runs_db.recent_runs(
|
|
2052
2088
|
limit, offset, search=search, main_context=self.context)
|
|
2089
|
+
now = datetime.now(timezone.utc)
|
|
2053
2090
|
for row in rows:
|
|
2054
2091
|
try:
|
|
2055
2092
|
elapsed = (datetime.fromisoformat(row["ended_at"])
|
|
@@ -2057,6 +2094,8 @@ class AdminService:
|
|
|
2057
2094
|
except (TypeError, ValueError):
|
|
2058
2095
|
elapsed = 0
|
|
2059
2096
|
row["duration_label"] = runs_db.fmt_elapsed(max(int(elapsed), 0))
|
|
2097
|
+
row["relative_age_label"] = relative_age_label(
|
|
2098
|
+
row["ended_at"], now)
|
|
2060
2099
|
return rows
|
|
2061
2100
|
|
|
2062
2101
|
def load_settings(self) -> Settings:
|
|
@@ -21,6 +21,7 @@ from codee_tasks_abstract.provider import AbstractTasksProvider
|
|
|
21
21
|
|
|
22
22
|
from codee.coding_agents import resolve_agent_code
|
|
23
23
|
from codee.lib import claude_key_rotation, runs_db
|
|
24
|
+
from codee.lib.runtime_control import is_paused
|
|
24
25
|
from codee.lib.trigger_aws_sqs_skills import trigger_aws_sqs_skills
|
|
25
26
|
from codee.lib.trigger_cron_skills import trigger_cron_skills
|
|
26
27
|
from codee.lib.trigger_email_skills import trigger_email_skills
|
|
@@ -359,6 +360,9 @@ def _submit_task(task_id: str, message: str, session_id: str, skill_name: str,
|
|
|
359
360
|
against the next tick.
|
|
360
361
|
"""
|
|
361
362
|
with _inflight_lock:
|
|
363
|
+
if is_paused(context):
|
|
364
|
+
log.debug("New agent work was paused before %s could start.", task_id)
|
|
365
|
+
return False
|
|
362
366
|
if task_id in _inflight:
|
|
363
367
|
log.debug("%s already running; skipping duplicate launch.", task_id)
|
|
364
368
|
return False
|
|
@@ -387,6 +391,10 @@ def run_once() -> None:
|
|
|
387
391
|
if not _pull_latest_code():
|
|
388
392
|
log.warning("Failed to pull from the repo, still continuing...")
|
|
389
393
|
|
|
394
|
+
if is_paused(context):
|
|
395
|
+
log.info("New agent work is paused; active agents continue running.")
|
|
396
|
+
return
|
|
397
|
+
|
|
390
398
|
trigger_cron_skills(_run_agent, main_context=context)
|
|
391
399
|
trigger_aws_sqs_skills(_run_agent, main_context=context)
|
|
392
400
|
trigger_email_skills(_run_agent, main_context=context)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Persistent controls shared by the admin UI and executor process."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codee_main_context.context import CodeeMainContext
|
|
5
|
+
|
|
6
|
+
PAUSED_FILE = "paused"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_paused(context: CodeeMainContext) -> bool:
|
|
10
|
+
"""Return whether new agent work is paused."""
|
|
11
|
+
return (context.data_dir / PAUSED_FILE).exists()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_paused(context: CodeeMainContext, paused: bool) -> None:
|
|
15
|
+
"""Persist whether new agent work may start."""
|
|
16
|
+
path = context.data_dir / PAUSED_FILE
|
|
17
|
+
if paused:
|
|
18
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
path.touch(exist_ok=True)
|
|
20
|
+
else:
|
|
21
|
+
path.unlink(missing_ok=True)
|
|
@@ -5,6 +5,7 @@ import tempfile
|
|
|
5
5
|
import threading
|
|
6
6
|
import time
|
|
7
7
|
import unittest
|
|
8
|
+
from datetime import datetime, timezone
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from unittest.mock import Mock, patch
|
|
10
11
|
from urllib.parse import parse_qs, urlparse
|
|
@@ -19,7 +20,7 @@ from codee.admin_service import (
|
|
|
19
20
|
USAGE_RATE_LIMIT_BACKOFF_SECONDS, WORKFLOW_CACHE_VERSION,
|
|
20
21
|
WORKFLOW_HUMAN_EDGE_COLOR, AdminService, WorkflowGeneration,
|
|
21
22
|
_remove_redundant_skill_transitions, azure_oauth, issue_prompt_task,
|
|
22
|
-
normalize_work_items, parse_skill, repository_name)
|
|
23
|
+
normalize_work_items, parse_skill, relative_age_label, repository_name)
|
|
23
24
|
from codee.lib import runs_db
|
|
24
25
|
from codee_agent_claude_code import oauth as claude_oauth
|
|
25
26
|
from codee_agent_claude_code.account import AccountUnavailable
|
|
@@ -61,6 +62,30 @@ def _write_issue_skill(root: Path) -> Path:
|
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
class RecentRunsTest(unittest.TestCase):
|
|
65
|
+
def test_dashboard_pause_state_persists(self) -> None:
|
|
66
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
67
|
+
service = AdminService.__new__(AdminService)
|
|
68
|
+
service.data_dir = Path(directory)
|
|
69
|
+
service.context = CodeeMainContext(data_dir=service.data_dir)
|
|
70
|
+
|
|
71
|
+
self.assertFalse(service.set_paused(False))
|
|
72
|
+
self.assertTrue(service.set_paused(True))
|
|
73
|
+
self.assertTrue(service.dashboard()["paused"])
|
|
74
|
+
self.assertFalse(service.set_paused(False))
|
|
75
|
+
|
|
76
|
+
def test_formats_relative_age_in_minutes_hours_and_days(self) -> None:
|
|
77
|
+
now = datetime(2026, 9, 24, 12, 0, tzinfo=timezone.utc)
|
|
78
|
+
cases = [
|
|
79
|
+
("2026-09-24T11:59:30+00:00", "1 minute ago"),
|
|
80
|
+
("2026-09-24T11:58:00+00:00", "2 minutes ago"),
|
|
81
|
+
("2026-09-24T10:00:00+00:00", "2 hours ago"),
|
|
82
|
+
("2026-09-21T12:00:00+00:00", "3 days ago"),
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
for timestamp, expected in cases:
|
|
86
|
+
with self.subTest(timestamp=timestamp):
|
|
87
|
+
self.assertEqual(relative_age_label(timestamp, now), expected)
|
|
88
|
+
|
|
64
89
|
def test_formats_duration_from_start_and_end_times(self) -> None:
|
|
65
90
|
with tempfile.TemporaryDirectory() as directory:
|
|
66
91
|
context = CodeeMainContext(data_dir=Path(directory))
|
|
@@ -1281,18 +1306,25 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1281
1306
|
"Move the story to CR Needed when every subtask is implemented.\n"
|
|
1282
1307
|
)
|
|
1283
1308
|
agent = Mock()
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1309
|
+
|
|
1310
|
+
def invalid_workflow(message, session_id, model, on_session_id):
|
|
1311
|
+
on_session_id("agent-thread")
|
|
1312
|
+
return (
|
|
1313
|
+
'{"statuses":["Ready","Subtask Review","CR Needed"],'
|
|
1314
|
+
'"transitions":[{"source":"Ready","target":"Subtask Review",'
|
|
1315
|
+
'"label":"story-developer","evidence":"Move each subtask to '
|
|
1316
|
+
'Subtask Review once its pull request is open."}],'
|
|
1317
|
+
'"final_statuses":["CR Needed"]}'
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
agent.run.side_effect = invalid_workflow
|
|
1321
|
+
agent.continue_conversation.return_value = (
|
|
1290
1322
|
'{"statuses":["Ready","CR Needed"],'
|
|
1291
1323
|
'"transitions":[{"source":"Ready","target":"CR Needed",'
|
|
1292
1324
|
'"label":"story-developer","evidence":"Move the story to CR '
|
|
1293
1325
|
'Needed when every subtask is implemented."}],'
|
|
1294
|
-
'"final_statuses":["CR Needed"]}'
|
|
1295
|
-
|
|
1326
|
+
'"final_statuses":["CR Needed"]}'
|
|
1327
|
+
)
|
|
1296
1328
|
service = AdminService.__new__(AdminService)
|
|
1297
1329
|
service.root = root
|
|
1298
1330
|
service.skills_dir = skills_dir
|
|
@@ -1305,14 +1337,23 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1305
1337
|
}):
|
|
1306
1338
|
workflow = service.generate_workflow()["story"]
|
|
1307
1339
|
|
|
1308
|
-
self.assertEqual(agent.run.call_count,
|
|
1340
|
+
self.assertEqual(agent.run.call_count, 1)
|
|
1341
|
+
self.assertEqual(agent.continue_conversation.call_count, 1)
|
|
1309
1342
|
self.assertIn(
|
|
1310
1343
|
"moves the subtask rather than the story",
|
|
1311
|
-
agent.
|
|
1344
|
+
agent.continue_conversation.call_args.args[0],
|
|
1312
1345
|
)
|
|
1313
1346
|
self.assertIn(
|
|
1314
1347
|
"The graph is the lifecycle of the story work item alone",
|
|
1315
|
-
agent.run.
|
|
1348
|
+
agent.run.call_args.args[0],
|
|
1349
|
+
)
|
|
1350
|
+
self.assertNotIn(
|
|
1351
|
+
"The graph is the lifecycle of the story work item alone",
|
|
1352
|
+
agent.continue_conversation.call_args.args[0],
|
|
1353
|
+
)
|
|
1354
|
+
self.assertEqual(
|
|
1355
|
+
agent.continue_conversation.call_args.args[1],
|
|
1356
|
+
"agent-thread",
|
|
1316
1357
|
)
|
|
1317
1358
|
self.assertEqual(
|
|
1318
1359
|
[node["data"]["label"] for node in workflow["nodes"]],
|
|
@@ -1382,12 +1423,14 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1382
1423
|
"After planning, move the story to [AI] Ready for human review.\n"
|
|
1383
1424
|
)
|
|
1384
1425
|
agent = Mock()
|
|
1385
|
-
agent.run.
|
|
1426
|
+
agent.run.return_value = (
|
|
1386
1427
|
'{"statuses":["[AI] Decomposition needed",'
|
|
1387
1428
|
'"[AI] Ready for development","[AI] Ready for human review"],'
|
|
1388
1429
|
'"transitions":[{"source":"[AI] Decomposition needed",'
|
|
1389
1430
|
'"target":"[AI] Ready for development","label":"",'
|
|
1390
|
-
'"evidence":""}],"final_statuses":[]}'
|
|
1431
|
+
'"evidence":""}],"final_statuses":[]}'
|
|
1432
|
+
)
|
|
1433
|
+
agent.continue_conversation.return_value = (
|
|
1391
1434
|
'{"statuses":["[AI] Decomposition needed",'
|
|
1392
1435
|
'"[AI] Ready for development","[AI] Ready for human review"],'
|
|
1393
1436
|
'"transitions":[{'
|
|
@@ -1395,8 +1438,8 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1395
1438
|
'"target":"[AI] Ready for human review",'
|
|
1396
1439
|
'"label":"story-planner",'
|
|
1397
1440
|
'"evidence":"After planning, move the story to [AI] Ready for human review."}],'
|
|
1398
|
-
'"final_statuses":["[AI] Ready for human review"]}'
|
|
1399
|
-
|
|
1441
|
+
'"final_statuses":["[AI] Ready for human review"]}'
|
|
1442
|
+
)
|
|
1400
1443
|
service = AdminService.__new__(AdminService)
|
|
1401
1444
|
service.root = root
|
|
1402
1445
|
service.skills_dir = skills_dir
|
|
@@ -1409,8 +1452,9 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1409
1452
|
}):
|
|
1410
1453
|
workflow = service.generate_workflow()["story"]
|
|
1411
1454
|
|
|
1412
|
-
self.assertEqual(agent.run.call_count,
|
|
1413
|
-
|
|
1455
|
+
self.assertEqual(agent.run.call_count, 1)
|
|
1456
|
+
self.assertEqual(agent.continue_conversation.call_count, 1)
|
|
1457
|
+
retry_prompt = agent.continue_conversation.call_args.args[0]
|
|
1414
1458
|
self.assertIn(
|
|
1415
1459
|
'transition 1 ("[AI] Decomposition needed" -> '
|
|
1416
1460
|
'"[AI] Ready for development", label "") is labelled with a '
|
|
@@ -1501,16 +1545,18 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1501
1545
|
"Move the issue to Review when work is done.\n"
|
|
1502
1546
|
)
|
|
1503
1547
|
agent = Mock()
|
|
1504
|
-
agent.run.
|
|
1548
|
+
agent.run.return_value = (
|
|
1505
1549
|
'{"statuses":["Ready","Review"],"transitions":['
|
|
1506
1550
|
'{"source":"Ready","target":"Review","label":"develop",'
|
|
1507
1551
|
'"evidence":"Work the issue until it is done."}],'
|
|
1508
|
-
'"final_statuses":["Review"]}'
|
|
1552
|
+
'"final_statuses":["Review"]}'
|
|
1553
|
+
)
|
|
1554
|
+
agent.continue_conversation.return_value = (
|
|
1509
1555
|
'{"statuses":["Ready","Review"],"transitions":['
|
|
1510
1556
|
'{"source":"Ready","target":"Review","label":"develop",'
|
|
1511
1557
|
'"evidence":"Move the issue to Review when work is done."}],'
|
|
1512
|
-
'"final_statuses":["Review"]}'
|
|
1513
|
-
|
|
1558
|
+
'"final_statuses":["Review"]}'
|
|
1559
|
+
)
|
|
1514
1560
|
service = AdminService.__new__(AdminService)
|
|
1515
1561
|
service.root = root
|
|
1516
1562
|
service.skills_dir = skills_dir
|
|
@@ -1554,18 +1600,20 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1554
1600
|
"Move the issue to Review when work is done.\n"
|
|
1555
1601
|
)
|
|
1556
1602
|
agent = Mock()
|
|
1557
|
-
agent.run.
|
|
1603
|
+
agent.run.return_value = (
|
|
1558
1604
|
'{"statuses":["Ready","Review"],"transitions":['
|
|
1559
1605
|
'{"source":"Blocked","target":"Review","label":"develop",'
|
|
1560
1606
|
'"evidence":"Move the issue to Review when work is done."},'
|
|
1561
1607
|
'{"source":"Ready","target":"Review","label":"develop",'
|
|
1562
1608
|
'"evidence":"Work the issue until it is done."}],'
|
|
1563
|
-
'"final_statuses":["Done"]}'
|
|
1609
|
+
'"final_statuses":["Done"]}'
|
|
1610
|
+
)
|
|
1611
|
+
agent.continue_conversation.return_value = (
|
|
1564
1612
|
'{"statuses":["Ready","Review"],"transitions":['
|
|
1565
1613
|
'{"source":"Ready","target":"Review","label":"develop",'
|
|
1566
1614
|
'"evidence":"Move the issue to Review when work is done."}],'
|
|
1567
|
-
'"final_statuses":["Review"]}'
|
|
1568
|
-
|
|
1615
|
+
'"final_statuses":["Review"]}'
|
|
1616
|
+
)
|
|
1569
1617
|
service = AdminService.__new__(AdminService)
|
|
1570
1618
|
service.root = root
|
|
1571
1619
|
service.skills_dir = skills_dir
|
|
@@ -1578,7 +1626,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1578
1626
|
}):
|
|
1579
1627
|
service.generate_workflow()
|
|
1580
1628
|
|
|
1581
|
-
retry_prompt = agent.
|
|
1629
|
+
retry_prompt = agent.continue_conversation.call_args.args[0]
|
|
1582
1630
|
self.assertIn('transition 1 ("Blocked" -> "Review", label '
|
|
1583
1631
|
'"develop") uses "Blocked", which is missing from '
|
|
1584
1632
|
'statuses "Ready", "Review"', retry_prompt)
|
|
@@ -1601,16 +1649,18 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1601
1649
|
"Move the issue to Done after approval.\n"
|
|
1602
1650
|
)
|
|
1603
1651
|
agent = Mock()
|
|
1604
|
-
agent.run.
|
|
1652
|
+
agent.run.return_value = (
|
|
1605
1653
|
'{"statuses":["Ready","Review","Done"],"transitions":['
|
|
1606
1654
|
'{"source":"Ready","target":"Done","label":"review",'
|
|
1607
1655
|
'"evidence":"Move the issue to Done after approval."}],'
|
|
1608
|
-
'"final_statuses":["Done"]}'
|
|
1656
|
+
'"final_statuses":["Done"]}'
|
|
1657
|
+
)
|
|
1658
|
+
agent.continue_conversation.return_value = (
|
|
1609
1659
|
'{"statuses":["Ready","Review","Done"],"transitions":['
|
|
1610
1660
|
'{"source":"Review","target":"Done","label":"review",'
|
|
1611
1661
|
'"evidence":"Move the issue to Done after approval."}],'
|
|
1612
|
-
'"final_statuses":["Done"]}'
|
|
1613
|
-
|
|
1662
|
+
'"final_statuses":["Done"]}'
|
|
1663
|
+
)
|
|
1614
1664
|
service = AdminService.__new__(AdminService)
|
|
1615
1665
|
service.root = root
|
|
1616
1666
|
service.skills_dir = skills_dir
|
|
@@ -1628,7 +1678,7 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1628
1678
|
'a status "review" never has the issue in: its entry statuses '
|
|
1629
1679
|
'are "Review" and no other transition of that skill moves the '
|
|
1630
1680
|
'issue to "Ready".',
|
|
1631
|
-
agent.
|
|
1681
|
+
agent.continue_conversation.call_args.args[0],
|
|
1632
1682
|
)
|
|
1633
1683
|
|
|
1634
1684
|
def test_generate_workflow_warns_when_statuses_have_no_edges(self) -> None:
|
|
@@ -1840,15 +1890,17 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1840
1890
|
"Move the issue to In Progress when work starts.\n"
|
|
1841
1891
|
)
|
|
1842
1892
|
agent = Mock()
|
|
1843
|
-
agent.run.
|
|
1893
|
+
agent.run.return_value = (
|
|
1844
1894
|
'{"statuses":["Ready","In Progress"],"transitions":[],'
|
|
1845
1895
|
'"final_statuses":[],"human_actions":[],'
|
|
1846
1896
|
'"human_transitions":[{"source":"Ready","target":"In Progress",'
|
|
1847
|
-
'"evidence":"Move the issue to In Progress when work starts."}]}'
|
|
1897
|
+
'"evidence":"Move the issue to In Progress when work starts."}]}'
|
|
1898
|
+
)
|
|
1899
|
+
agent.continue_conversation.return_value = (
|
|
1848
1900
|
'{"statuses":["Ready","In Progress"],"transitions":[],'
|
|
1849
1901
|
'"final_statuses":[],"human_actions":[],'
|
|
1850
|
-
'"human_transitions":[]}'
|
|
1851
|
-
|
|
1902
|
+
'"human_transitions":[]}'
|
|
1903
|
+
)
|
|
1852
1904
|
service = AdminService.__new__(AdminService)
|
|
1853
1905
|
service.root = root
|
|
1854
1906
|
service.skills_dir = skills_dir
|
|
@@ -1861,10 +1913,11 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1861
1913
|
}):
|
|
1862
1914
|
service.generate_workflow()
|
|
1863
1915
|
|
|
1864
|
-
self.assertEqual(agent.run.call_count,
|
|
1916
|
+
self.assertEqual(agent.run.call_count, 1)
|
|
1917
|
+
self.assertEqual(agent.continue_conversation.call_count, 1)
|
|
1865
1918
|
self.assertIn(
|
|
1866
1919
|
"human transition source Ready is an entry status of a skill",
|
|
1867
|
-
agent.
|
|
1920
|
+
agent.continue_conversation.call_args.args[0],
|
|
1868
1921
|
)
|
|
1869
1922
|
|
|
1870
1923
|
def test_generate_workflow_rejects_a_human_action_for_an_unknown_status(
|
|
@@ -1882,13 +1935,15 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1882
1935
|
"Move the issue to In Progress when work starts.\n"
|
|
1883
1936
|
)
|
|
1884
1937
|
agent = Mock()
|
|
1885
|
-
agent.run.
|
|
1938
|
+
agent.run.return_value = (
|
|
1886
1939
|
'{"statuses":["Ready","In Progress"],"transitions":[],'
|
|
1887
1940
|
'"final_statuses":[],"human_actions":[{"status":"Blocked",'
|
|
1888
|
-
'"action":"Unblock the story."}]}'
|
|
1941
|
+
'"action":"Unblock the story."}]}'
|
|
1942
|
+
)
|
|
1943
|
+
agent.continue_conversation.return_value = (
|
|
1889
1944
|
'{"statuses":["Ready","In Progress"],"transitions":[],'
|
|
1890
|
-
'"final_statuses":[],"human_actions":[]}'
|
|
1891
|
-
|
|
1945
|
+
'"final_statuses":[],"human_actions":[]}'
|
|
1946
|
+
)
|
|
1892
1947
|
service = AdminService.__new__(AdminService)
|
|
1893
1948
|
service.root = root
|
|
1894
1949
|
service.skills_dir = skills_dir
|
|
@@ -1901,10 +1956,11 @@ class AdminServiceIssueTriggerTest(unittest.TestCase):
|
|
|
1901
1956
|
}):
|
|
1902
1957
|
service.generate_workflow()
|
|
1903
1958
|
|
|
1904
|
-
self.assertEqual(agent.run.call_count,
|
|
1959
|
+
self.assertEqual(agent.run.call_count, 1)
|
|
1960
|
+
self.assertEqual(agent.continue_conversation.call_count, 1)
|
|
1905
1961
|
self.assertIn(
|
|
1906
1962
|
"each human action must name a declared status",
|
|
1907
|
-
agent.
|
|
1963
|
+
agent.continue_conversation.call_args.args[0],
|
|
1908
1964
|
)
|
|
1909
1965
|
|
|
1910
1966
|
def test_generate_workflow_returns_empty_without_issue_skills(self) -> None:
|
|
@@ -533,6 +533,27 @@ class ParentWorkItemSkipTest(unittest.TestCase):
|
|
|
533
533
|
executor.run_once()
|
|
534
534
|
return [call.args[0] for call in submit.call_args_list]
|
|
535
535
|
|
|
536
|
+
def test_paused_executor_starts_no_trigger_or_issue_work(self) -> None:
|
|
537
|
+
with patch.object(executor, "is_paused", return_value=True), \
|
|
538
|
+
patch.object(executor, "trigger_cron_skills") as cron, \
|
|
539
|
+
patch.object(executor, "trigger_aws_sqs_skills") as sqs, \
|
|
540
|
+
patch.object(executor, "trigger_email_skills") as email:
|
|
541
|
+
executor.run_once()
|
|
542
|
+
|
|
543
|
+
cron.assert_not_called()
|
|
544
|
+
sqs.assert_not_called()
|
|
545
|
+
email.assert_not_called()
|
|
546
|
+
self.provider.get_tasks.assert_not_called()
|
|
547
|
+
|
|
548
|
+
def test_pause_while_polling_prevents_issue_launch(self) -> None:
|
|
549
|
+
with patch.object(executor, "is_paused", return_value=True), \
|
|
550
|
+
patch.object(executor, "_run_task") as run_task:
|
|
551
|
+
launched = executor._submit_task(
|
|
552
|
+
"NIM-4", "prompt", "session", "Task developer")
|
|
553
|
+
|
|
554
|
+
self.assertFalse(launched)
|
|
555
|
+
run_task.assert_not_called()
|
|
556
|
+
|
|
536
557
|
def test_an_item_under_a_mapped_parent_is_left_to_that_parents_run(self) -> None:
|
|
537
558
|
self.assertEqual(
|
|
538
559
|
self._submitted(_polled_task("NIM-1", parent_type="Story")), [])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/aws-sqs-alarm-response/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/cron-research-5xx-errors/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/story-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-code-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codee_agent-0.7.0 → codee_agent-0.7.2}/src/codee/templates/skills/task-security-reviewer/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|