From 3a2bc5135ef033b53c7ad8de615af1893d4e7701 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Feb 2026 19:08:14 +0530 Subject: [PATCH] feat(agents): Update task comment requirements and add in_progress_at tracking --- .../c1a2b3c4d5e7_add_task_in_progress_at.py | 31 +++++++ backend/app/api/tasks.py | 80 ++++++++++++++++++- backend/app/models/tasks.py | 1 + backend/app/schemas/tasks.py | 2 + templates/AGENTS.md | 11 ++- templates/BOOT.md | 3 +- templates/HEARTBEAT.md | 29 +++++-- 7 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 backend/alembic/versions/c1a2b3c4d5e7_add_task_in_progress_at.py diff --git a/backend/alembic/versions/c1a2b3c4d5e7_add_task_in_progress_at.py b/backend/alembic/versions/c1a2b3c4d5e7_add_task_in_progress_at.py new file mode 100644 index 0000000..8f09ca2 --- /dev/null +++ b/backend/alembic/versions/c1a2b3c4d5e7_add_task_in_progress_at.py @@ -0,0 +1,31 @@ +"""add task in_progress_at + +Revision ID: c1a2b3c4d5e7 +Revises: b9d22e2a4d50 +Create Date: 2026-02-04 13:34:25.000000 + +""" +from __future__ import annotations + +from alembic import op + + +# revision identifiers, used by Alembic. +revision = "c1a2b3c4d5e7" +down_revision = "b9d22e2a4d50" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + "ALTER TABLE tasks ADD COLUMN IF NOT EXISTS in_progress_at TIMESTAMP WITHOUT TIME ZONE" + ) + op.execute( + "CREATE INDEX IF NOT EXISTS ix_tasks_in_progress_at ON tasks (in_progress_at)" + ) + + +def downgrade() -> None: + op.execute("DROP INDEX IF EXISTS ix_tasks_in_progress_at") + op.execute("ALTER TABLE tasks DROP COLUMN IF EXISTS in_progress_at") diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index b2bfd99..6fd77b2 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -1,9 +1,10 @@ from __future__ import annotations from datetime import datetime +from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy import asc +from sqlalchemy import asc, desc from sqlmodel import Session, col, select from app.api.deps import ( @@ -30,6 +31,42 @@ from app.services.activity_log import record_activity router = APIRouter(prefix="/boards/{board_id}/tasks", tags=["tasks"]) +REQUIRED_COMMENT_FIELDS = ("summary:", "details:", "next:") + + +def is_valid_markdown_comment(message: str) -> bool: + content = message.strip() + if not content: + return False + lowered = content.lower() + if not all(field in lowered for field in REQUIRED_COMMENT_FIELDS): + return False + if "- " not in content and "* " not in content: + return False + return True + + +def has_valid_recent_comment( + session: Session, + task: Task, + agent_id: UUID | None, + since: datetime | None, +) -> bool: + if agent_id is None or since is None: + return False + statement = ( + select(ActivityEvent) + .where(col(ActivityEvent.task_id) == task.id) + .where(col(ActivityEvent.event_type) == "task.comment") + .where(col(ActivityEvent.agent_id) == agent_id) + .where(col(ActivityEvent.created_at) >= since) + .order_by(desc(col(ActivityEvent.created_at))) + ) + event = session.exec(statement).first() + if event is None or event.message is None: + return False + return is_valid_markdown_comment(event.message) + @router.get("", response_model=list[TaskRead]) def list_tasks( @@ -74,20 +111,28 @@ def update_task( ) -> Task: previous_status = task.status updates = payload.model_dump(exclude_unset=True) + comment = updates.pop("comment", None) if actor.actor_type == "agent": if actor.agent and actor.agent.board_id and task.board_id: if actor.agent.board_id != task.board_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - allowed_fields = {"status"} + allowed_fields = {"status", "comment"} if not set(updates).issubset(allowed_fields): raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) if "status" in updates: if updates["status"] == "inbox": task.assigned_agent_id = None + task.in_progress_at = None else: task.assigned_agent_id = actor.agent.id if actor.agent else None - elif "status" in updates and updates["status"] == "inbox": - task.assigned_agent_id = None + if updates["status"] == "in_progress": + task.in_progress_at = datetime.utcnow() + elif "status" in updates: + if updates["status"] == "inbox": + task.assigned_agent_id = None + task.in_progress_at = None + elif updates["status"] == "in_progress": + task.in_progress_at = datetime.utcnow() if "assigned_agent_id" in updates and updates["assigned_agent_id"]: agent = session.get(Agent, updates["assigned_agent_id"]) if agent is None: @@ -98,10 +143,35 @@ def update_task( setattr(task, key, value) task.updated_at = datetime.utcnow() + if "status" in updates and updates["status"] == "review": + if comment is not None and comment.strip(): + if not is_valid_markdown_comment(comment): + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + else: + if not has_valid_recent_comment( + session, + task, + task.assigned_agent_id, + task.in_progress_at, + ): + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + session.add(task) session.commit() session.refresh(task) + if comment is not None and comment.strip(): + if actor.actor_type == "agent" and not is_valid_markdown_comment(comment): + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + event = ActivityEvent( + event_type="task.comment", + message=comment.strip(), + task_id=task.id, + agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None, + ) + session.add(event) + session.commit() + if "status" in updates and task.status != previous_status: event_type = "task.status_changed" message = f"Task moved to {task.status}: {task.title}." @@ -160,6 +230,8 @@ def create_task_comment( raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) if not payload.message.strip(): raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + if actor.actor_type == "agent" and not is_valid_markdown_comment(payload.message): + raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) event = ActivityEvent( event_type="task.comment", message=payload.message.strip(), diff --git a/backend/app/models/tasks.py b/backend/app/models/tasks.py index 31f9a57..1c35df2 100644 --- a/backend/app/models/tasks.py +++ b/backend/app/models/tasks.py @@ -19,6 +19,7 @@ class Task(TenantScoped, table=True): status: str = Field(default="inbox", index=True) priority: str = Field(default="medium", index=True) due_at: datetime | None = None + in_progress_at: datetime | None = None created_by_user_id: UUID | None = Field(default=None, foreign_key="users.id", index=True) assigned_agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) diff --git a/backend/app/schemas/tasks.py b/backend/app/schemas/tasks.py index 817ee87..b1b26fe 100644 --- a/backend/app/schemas/tasks.py +++ b/backend/app/schemas/tasks.py @@ -26,12 +26,14 @@ class TaskUpdate(SQLModel): priority: str | None = None due_at: datetime | None = None assigned_agent_id: UUID | None = None + comment: str | None = None class TaskRead(TaskBase): id: UUID board_id: UUID | None created_by_user_id: UUID | None + in_progress_at: datetime | None created_at: datetime updated_at: datetime diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 55aef61..ac4aeed 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -30,5 +30,12 @@ Write things down. Do not rely on short-term context. - HEARTBEAT.md defines what to do on each heartbeat. ## Task updates -- Log all task progress and results via the task comments endpoint. -- Do not post task updates in chat/web channels. +- All task updates MUST be posted to the task comments endpoint. +- Do not post task updates in chat/web channels under any circumstance. +- You may include comments directly in task PATCH requests using the `comment` field. +- Required comment fields (markdown): + - `status`: inbox | in_progress | review | done + - `summary`: one line + - `details`: 1–3 bullets + - `next`: next step or handoff request +- Every status change must include a comment within 30 seconds (see HEARTBEAT.md). diff --git a/templates/BOOT.md b/templates/BOOT.md index 8f17c33..8ba945d 100644 --- a/templates/BOOT.md +++ b/templates/BOOT.md @@ -4,6 +4,7 @@ On startup: 1) Verify API reachability (GET {{ base_url }}/api/v1/gateway/status). - A 401 Unauthorized response is acceptable here for agents (auth-protected endpoint). 2) Connect to Mission Control once by sending a heartbeat check-in. -2a) Use task comments for updates; do not send task updates to chat/web. +2a) Use task comments for all updates; do not send task updates to chat/web. +2b) Follow the required comment format in AGENTS.md / HEARTBEAT.md. 3) If you send a boot message, end with NO_REPLY. 4) If BOOTSTRAP.md exists in this workspace, the agent should run it once and delete it. diff --git a/templates/HEARTBEAT.md b/templates/HEARTBEAT.md index a54e199..34494e2 100644 --- a/templates/HEARTBEAT.md +++ b/templates/HEARTBEAT.md @@ -21,6 +21,15 @@ curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \ -d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}' ``` +## Commenting rules (mandatory) +- Every task state change MUST be followed by a task comment within 30 seconds. +- Never post task updates to chat/web channels. Task comments are the only update channel. +- Minimum comment format: + - `status`: inbox | in_progress | review | done + - `summary`: one-line progress update + - `details`: 1–3 bullets of what changed / what you did + - `next`: next step or handoff request + 2) List boards: ```bash curl -s "$BASE_URL/api/v1/boards" \ @@ -40,25 +49,29 @@ curl -s "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks" \ curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \ -H "X-Agent-Token: $AUTH_TOKEN" \ -H "Content-Type: application/json" \ - -d '{"status": "in_progress"}' + -d '{"status": "in_progress", "comment": "[status=in_progress] Claimed by '$AGENT_NAME'.\\nsummary: Starting work.\\ndetails: - Triage task and plan approach.\\nnext: Begin execution."}' ``` 5) Work the task: - Update status as you progress. - Post a brief work log to the task comments endpoint (do not use chat). -- When complete, move to "review": +- When complete, use the following mandatory steps: + +5a) Post the completion comment (required, markdown). Include: +- status, summary, details (bullets), next, and the full response text. +Use the task comments endpoint for this step. + +5b) Move the task to "review": ```bash curl -s -X PATCH "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}" \ -H "X-Agent-Token: $AUTH_TOKEN" \ -H "Content-Type: application/json" \ -d '{"status": "review"}' ``` -```bash -curl -s -X POST "$BASE_URL/api/v1/boards/{BOARD_ID}/tasks/{TASK_ID}/comments" \ - -H "X-Agent-Token: $AUTH_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"message": "Summary of work, result, and next steps."}' -``` + +## Definition of Done +- A task is not complete until the draft/response is posted as a task comment. +- Comments must be markdown and include: summary, details (bullets), next. ## Status flow ```