mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-05-06 16:26:16 +08:00
Simplify task graph to single-source blockedBy (PR #127)
Remove unused blocks field and add_blocks parameter. The LLM never used add_blocks in practice, making it dead code that taught a misleading bidirectional pattern. Replace with remove_blocked_by for dependency rewiring. Single-source-of-truth with blockedBy only.
This commit is contained in:
@@ -4,12 +4,12 @@
|
|||||||
s07_task_system.py - Tasks
|
s07_task_system.py - Tasks
|
||||||
|
|
||||||
Tasks persist as JSON files in .tasks/ so they survive context compression.
|
Tasks persist as JSON files in .tasks/ so they survive context compression.
|
||||||
Each task has a dependency graph (blockedBy/blocks).
|
Each task has a dependency graph (blockedBy).
|
||||||
|
|
||||||
.tasks/
|
.tasks/
|
||||||
task_1.json {"id":1, "subject":"...", "status":"completed", ...}
|
task_1.json {"id":1, "subject":"...", "status":"completed", ...}
|
||||||
task_2.json {"id":2, "blockedBy":[1], "status":"pending", ...}
|
task_2.json {"id":2, "blockedBy":[1], "status":"pending", ...}
|
||||||
task_3.json {"id":3, "blockedBy":[2], "blocks":[], ...}
|
task_3.json {"id":3, "blockedBy":[2], ...}
|
||||||
|
|
||||||
Dependency resolution:
|
Dependency resolution:
|
||||||
+----------+ +----------+ +----------+
|
+----------+ +----------+ +----------+
|
||||||
@@ -67,7 +67,7 @@ class TaskManager:
|
|||||||
def create(self, subject: str, description: str = "") -> str:
|
def create(self, subject: str, description: str = "") -> str:
|
||||||
task = {
|
task = {
|
||||||
"id": self._next_id, "subject": subject, "description": description,
|
"id": self._next_id, "subject": subject, "description": description,
|
||||||
"status": "pending", "blockedBy": [], "blocks": [], "owner": "",
|
"status": "pending", "blockedBy": [], "owner": "",
|
||||||
}
|
}
|
||||||
self._save(task)
|
self._save(task)
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
@@ -77,37 +77,18 @@ class TaskManager:
|
|||||||
return json.dumps(self._load(task_id), indent=2, ensure_ascii=False)
|
return json.dumps(self._load(task_id), indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
def update(self, task_id: int, status: str = None,
|
def update(self, task_id: int, status: str = None,
|
||||||
add_blocked_by: list = None, add_blocks: list = None) -> str:
|
add_blocked_by: list = None, remove_blocked_by: list = None) -> str:
|
||||||
task = self._load(task_id)
|
task = self._load(task_id)
|
||||||
if status:
|
if status:
|
||||||
if status not in ("pending", "in_progress", "completed"):
|
if status not in ("pending", "in_progress", "completed"):
|
||||||
raise ValueError(f"Invalid status: {status}")
|
raise ValueError(f"Invalid status: {status}")
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
# When a task is completed, remove it from all other tasks' blockedBy
|
|
||||||
if status == "completed":
|
if status == "completed":
|
||||||
self._clear_dependency(task_id)
|
self._clear_dependency(task_id)
|
||||||
if add_blocked_by:
|
if add_blocked_by:
|
||||||
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
||||||
# Bidirectional: also update the blocker tasks' blocks lists
|
if remove_blocked_by:
|
||||||
for blocker_id in add_blocked_by:
|
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
|
||||||
try:
|
|
||||||
blocker = self._load(blocker_id)
|
|
||||||
if task_id not in blocker["blocks"]:
|
|
||||||
blocker["blocks"].append(task_id)
|
|
||||||
self._save(blocker)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if add_blocks:
|
|
||||||
task["blocks"] = list(set(task["blocks"] + add_blocks))
|
|
||||||
# Bidirectional: also update the blocked tasks' blockedBy lists
|
|
||||||
for blocked_id in add_blocks:
|
|
||||||
try:
|
|
||||||
blocked = self._load(blocked_id)
|
|
||||||
if task_id not in blocked["blockedBy"]:
|
|
||||||
blocked["blockedBy"].append(task_id)
|
|
||||||
self._save(blocked)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
self._save(task)
|
self._save(task)
|
||||||
return json.dumps(task, indent=2, ensure_ascii=False)
|
return json.dumps(task, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
@@ -195,7 +176,7 @@ TOOL_HANDLERS = {
|
|||||||
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
|
"write_file": lambda **kw: run_write(kw["path"], kw["content"]),
|
||||||
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
|
"edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
|
||||||
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
|
"task_create": lambda **kw: TASKS.create(kw["subject"], kw.get("description", "")),
|
||||||
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("addBlocks")),
|
"task_update": lambda **kw: TASKS.update(kw["task_id"], kw.get("status"), kw.get("addBlockedBy"), kw.get("removeBlockedBy")),
|
||||||
"task_list": lambda **kw: TASKS.list_all(),
|
"task_list": lambda **kw: TASKS.list_all(),
|
||||||
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
|
"task_get": lambda **kw: TASKS.get(kw["task_id"]),
|
||||||
}
|
}
|
||||||
@@ -212,7 +193,7 @@ TOOLS = [
|
|||||||
{"name": "task_create", "description": "Create a new task.",
|
{"name": "task_create", "description": "Create a new task.",
|
||||||
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"]}},
|
"input_schema": {"type": "object", "properties": {"subject": {"type": "string"}, "description": {"type": "string"}}, "required": ["subject"]}},
|
||||||
{"name": "task_update", "description": "Update a task's status or dependencies.",
|
{"name": "task_update", "description": "Update a task's status or dependencies.",
|
||||||
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, "addBlocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
|
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed"]}, "addBlockedBy": {"type": "array", "items": {"type": "integer"}}, "removeBlockedBy": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
|
||||||
{"name": "task_list", "description": "List all tasks with status summary.",
|
{"name": "task_list", "description": "List all tasks with status summary.",
|
||||||
"input_schema": {"type": "object", "properties": {}}},
|
"input_schema": {"type": "object", "properties": {}}},
|
||||||
{"name": "task_get", "description": "Get full details of a task by ID.",
|
{"name": "task_get", "description": "Get full details of a task by ID.",
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ class TaskManager:
|
|||||||
|
|
||||||
def create(self, subject: str, description: str = "") -> str:
|
def create(self, subject: str, description: str = "") -> str:
|
||||||
task = {"id": self._next_id(), "subject": subject, "description": description,
|
task = {"id": self._next_id(), "subject": subject, "description": description,
|
||||||
"status": "pending", "owner": None, "blockedBy": [], "blocks": []}
|
"status": "pending", "owner": None, "blockedBy": []}
|
||||||
self._save(task)
|
self._save(task)
|
||||||
return json.dumps(task, indent=2)
|
return json.dumps(task, indent=2)
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ class TaskManager:
|
|||||||
return json.dumps(self._load(tid), indent=2)
|
return json.dumps(self._load(tid), indent=2)
|
||||||
|
|
||||||
def update(self, tid: int, status: str = None,
|
def update(self, tid: int, status: str = None,
|
||||||
add_blocked_by: list = None, add_blocks: list = None) -> str:
|
add_blocked_by: list = None, remove_blocked_by: list = None) -> str:
|
||||||
task = self._load(tid)
|
task = self._load(tid)
|
||||||
if status:
|
if status:
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
@@ -300,8 +300,8 @@ class TaskManager:
|
|||||||
return f"Task {tid} deleted"
|
return f"Task {tid} deleted"
|
||||||
if add_blocked_by:
|
if add_blocked_by:
|
||||||
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
||||||
if add_blocks:
|
if remove_blocked_by:
|
||||||
task["blocks"] = list(set(task["blocks"] + add_blocks))
|
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
|
||||||
self._save(task)
|
self._save(task)
|
||||||
return json.dumps(task, indent=2)
|
return json.dumps(task, indent=2)
|
||||||
|
|
||||||
@@ -587,7 +587,7 @@ TOOL_HANDLERS = {
|
|||||||
"check_background": lambda **kw: BG.check(kw.get("task_id")),
|
"check_background": lambda **kw: BG.check(kw.get("task_id")),
|
||||||
"task_create": lambda **kw: TASK_MGR.create(kw["subject"], kw.get("description", "")),
|
"task_create": lambda **kw: TASK_MGR.create(kw["subject"], kw.get("description", "")),
|
||||||
"task_get": lambda **kw: TASK_MGR.get(kw["task_id"]),
|
"task_get": lambda **kw: TASK_MGR.get(kw["task_id"]),
|
||||||
"task_update": lambda **kw: TASK_MGR.update(kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("add_blocks")),
|
"task_update": lambda **kw: TASK_MGR.update(kw["task_id"], kw.get("status"), kw.get("add_blocked_by"), kw.get("remove_blocked_by")),
|
||||||
"task_list": lambda **kw: TASK_MGR.list_all(),
|
"task_list": lambda **kw: TASK_MGR.list_all(),
|
||||||
"spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
|
"spawn_teammate": lambda **kw: TEAM.spawn(kw["name"], kw["role"], kw["prompt"]),
|
||||||
"list_teammates": lambda **kw: TEAM.list_all(),
|
"list_teammates": lambda **kw: TEAM.list_all(),
|
||||||
@@ -626,7 +626,7 @@ TOOLS = [
|
|||||||
{"name": "task_get", "description": "Get task details by ID.",
|
{"name": "task_get", "description": "Get task details by ID.",
|
||||||
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}},
|
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}}, "required": ["task_id"]}},
|
||||||
{"name": "task_update", "description": "Update task status or dependencies.",
|
{"name": "task_update", "description": "Update task status or dependencies.",
|
||||||
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "add_blocks": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
|
"input_schema": {"type": "object", "properties": {"task_id": {"type": "integer"}, "status": {"type": "string", "enum": ["pending", "in_progress", "completed", "deleted"]}, "add_blocked_by": {"type": "array", "items": {"type": "integer"}}, "remove_blocked_by": {"type": "array", "items": {"type": "integer"}}}, "required": ["task_id"]}},
|
||||||
{"name": "task_list", "description": "List all tasks.",
|
{"name": "task_list", "description": "List all tasks.",
|
||||||
"input_schema": {"type": "object", "properties": {}}},
|
"input_schema": {"type": "object", "properties": {}}},
|
||||||
{"name": "spawn_teammate", "description": "Spawn a persistent autonomous teammate.",
|
{"name": "spawn_teammate", "description": "Spawn a persistent autonomous teammate.",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Without explicit relationships, the agent can't tell what's ready, what's blocke
|
|||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
Promote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`), and dependents (`blocks`). The graph answers three questions at any moment:
|
Promote the checklist into a **task graph** persisted to disk. Each task is a JSON file with status, dependencies (`blockedBy`). The graph answers three questions at any moment:
|
||||||
|
|
||||||
- **What's ready?** -- tasks with `pending` status and empty `blockedBy`.
|
- **What's ready?** -- tasks with `pending` status and empty `blockedBy`.
|
||||||
- **What's blocked?** -- tasks waiting on unfinished dependencies.
|
- **What's blocked?** -- tasks waiting on unfinished dependencies.
|
||||||
@@ -60,7 +60,7 @@ class TaskManager:
|
|||||||
def create(self, subject, description=""):
|
def create(self, subject, description=""):
|
||||||
task = {"id": self._next_id, "subject": subject,
|
task = {"id": self._next_id, "subject": subject,
|
||||||
"status": "pending", "blockedBy": [],
|
"status": "pending", "blockedBy": [],
|
||||||
"blocks": [], "owner": ""}
|
"owner": ""}
|
||||||
self._save(task)
|
self._save(task)
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
return json.dumps(task, indent=2)
|
return json.dumps(task, indent=2)
|
||||||
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def update(self, task_id, status=None,
|
def update(self, task_id, status=None,
|
||||||
add_blocked_by=None, add_blocks=None):
|
add_blocked_by=None, remove_blocked_by=None):
|
||||||
task = self._load(task_id)
|
task = self._load(task_id)
|
||||||
if status:
|
if status:
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
if status == "completed":
|
if status == "completed":
|
||||||
self._clear_dependency(task_id)
|
self._clear_dependency(task_id)
|
||||||
|
if add_blocked_by:
|
||||||
|
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
||||||
|
if remove_blocked_by:
|
||||||
|
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
|
||||||
self._save(task)
|
self._save(task)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ From s07 onward, the task graph is the default for multi-step work. s03's Todo r
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
||||||
| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) |
|
| Planning model | Flat checklist (in-memory) | Task graph with dependencies (on disk) |
|
||||||
| Relationships | None | `blockedBy` + `blocks` edges |
|
| Relationships | None | `blockedBy` edges |
|
||||||
| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` |
|
| Status tracking | Done or not | `pending` -> `in_progress` -> `completed` |
|
||||||
| Persistence | Lost on compression | Survives compression and restarts |
|
| Persistence | Lost on compression | Survives compression and restarts |
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ s03のTodoManagerはメモリ上のフラットなチェックリストに過ぎ
|
|||||||
|
|
||||||
## 解決策
|
## 解決策
|
||||||
|
|
||||||
フラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)・後方依存(`blocks`)を持つ。タスクグラフは常に3つの問いに答える:
|
フラットなチェックリストをディスクに永続化する**タスクグラフ**に昇格させる。各タスクは1つのJSONファイルで、ステータス・前方依存(`blockedBy`)を持つ。タスクグラフは常に3つの問いに答える:
|
||||||
|
|
||||||
- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。
|
- **何が実行可能か?** -- `pending`ステータスで`blockedBy`が空のタスク。
|
||||||
- **何がブロックされているか?** -- 未完了の依存を待つタスク。
|
- **何がブロックされているか?** -- 未完了の依存を待つタスク。
|
||||||
@@ -60,7 +60,7 @@ class TaskManager:
|
|||||||
def create(self, subject, description=""):
|
def create(self, subject, description=""):
|
||||||
task = {"id": self._next_id, "subject": subject,
|
task = {"id": self._next_id, "subject": subject,
|
||||||
"status": "pending", "blockedBy": [],
|
"status": "pending", "blockedBy": [],
|
||||||
"blocks": [], "owner": ""}
|
"owner": ""}
|
||||||
self._save(task)
|
self._save(task)
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
return json.dumps(task, indent=2)
|
return json.dumps(task, indent=2)
|
||||||
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def update(self, task_id, status=None,
|
def update(self, task_id, status=None,
|
||||||
add_blocked_by=None, add_blocks=None):
|
add_blocked_by=None, remove_blocked_by=None):
|
||||||
task = self._load(task_id)
|
task = self._load(task_id)
|
||||||
if status:
|
if status:
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
if status == "completed":
|
if status == "completed":
|
||||||
self._clear_dependency(task_id)
|
self._clear_dependency(task_id)
|
||||||
|
if add_blocked_by:
|
||||||
|
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
||||||
|
if remove_blocked_by:
|
||||||
|
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
|
||||||
self._save(task)
|
self._save(task)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ s07以降、タスクグラフがマルチステップ作業のデフォルト
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
||||||
| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |
|
| 計画モデル | フラットチェックリスト (メモリ) | 依存関係付きタスクグラフ (ディスク) |
|
||||||
| 関係 | なし | `blockedBy` + `blocks` エッジ |
|
| 関係 | なし | `blockedBy` エッジ |
|
||||||
| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |
|
| ステータス追跡 | 完了か未完了 | `pending` -> `in_progress` -> `completed` |
|
||||||
| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |
|
| 永続性 | 圧縮で消失 | 圧縮・再起動後も存続 |
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ s03 的 TodoManager 只是内存中的扁平清单: 没有顺序、没有依赖
|
|||||||
|
|
||||||
## 解决方案
|
## 解决方案
|
||||||
|
|
||||||
把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`) 和后置依赖 (`blocks`)。任务图随时回答三个问题:
|
把扁平清单升级为持久化到磁盘的**任务图**。每个任务是一个 JSON 文件, 有状态、前置依赖 (`blockedBy`)。任务图随时回答三个问题:
|
||||||
|
|
||||||
- **什么可以做?** -- 状态为 `pending` 且 `blockedBy` 为空的任务。
|
- **什么可以做?** -- 状态为 `pending` 且 `blockedBy` 为空的任务。
|
||||||
- **什么被卡住?** -- 等待前置任务完成的任务。
|
- **什么被卡住?** -- 等待前置任务完成的任务。
|
||||||
@@ -60,7 +60,7 @@ class TaskManager:
|
|||||||
def create(self, subject, description=""):
|
def create(self, subject, description=""):
|
||||||
task = {"id": self._next_id, "subject": subject,
|
task = {"id": self._next_id, "subject": subject,
|
||||||
"status": "pending", "blockedBy": [],
|
"status": "pending", "blockedBy": [],
|
||||||
"blocks": [], "owner": ""}
|
"owner": ""}
|
||||||
self._save(task)
|
self._save(task)
|
||||||
self._next_id += 1
|
self._next_id += 1
|
||||||
return json.dumps(task, indent=2)
|
return json.dumps(task, indent=2)
|
||||||
@@ -81,12 +81,16 @@ def _clear_dependency(self, completed_id):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def update(self, task_id, status=None,
|
def update(self, task_id, status=None,
|
||||||
add_blocked_by=None, add_blocks=None):
|
add_blocked_by=None, remove_blocked_by=None):
|
||||||
task = self._load(task_id)
|
task = self._load(task_id)
|
||||||
if status:
|
if status:
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
if status == "completed":
|
if status == "completed":
|
||||||
self._clear_dependency(task_id)
|
self._clear_dependency(task_id)
|
||||||
|
if add_blocked_by:
|
||||||
|
task["blockedBy"] = list(set(task["blockedBy"] + add_blocked_by))
|
||||||
|
if remove_blocked_by:
|
||||||
|
task["blockedBy"] = [x for x in task["blockedBy"] if x not in remove_blocked_by]
|
||||||
self._save(task)
|
self._save(task)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -110,7 +114,7 @@ TOOL_HANDLERS = {
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
| Tools | 5 | 8 (`task_create/update/list/get`) |
|
||||||
| 规划模型 | 扁平清单 (仅内存) | 带依赖关系的任务图 (磁盘) |
|
| 规划模型 | 扁平清单 (仅内存) | 带依赖关系的任务图 (磁盘) |
|
||||||
| 关系 | 无 | `blockedBy` + `blocks` 边 |
|
| 关系 | 无 | `blockedBy` 边 |
|
||||||
| 状态追踪 | 做完没做完 | `pending` -> `in_progress` -> `completed` |
|
| 状态追踪 | 做完没做完 | `pending` -> `in_progress` -> `completed` |
|
||||||
| 持久化 | 压缩后丢失 | 压缩和重启后存活 |
|
| 持久化 | 压缩后丢失 | 压缩和重启后存活 |
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user