feat(agents): Update task comment requirements and add in_progress_at tracking

This commit is contained in:
Abhimanyu Saharan
2026-02-04 19:08:14 +05:30
parent 554ecc4c85
commit 3a2bc5135e
7 changed files with 142 additions and 15 deletions

View File

@@ -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")

View File

@@ -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(),

View File

@@ -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)

View File

@@ -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

View File

@@ -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`: 13 bullets
- `next`: next step or handoff request
- Every status change must include a comment within 30 seconds (see HEARTBEAT.md).

View File

@@ -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.

View File

@@ -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`: 13 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
```