Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/agent-coordination.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ The surface, by scope:
`POST .../tasks/{id}/(claim|release|close|reopen)`, and
`GET /api/projects/tasks/{id}/context`. This is read + lifecycle + comments
only. Granting project_tasks also makes the agent a project member.
`POST .../tasks/{id}/claimable` is also reachable, but LEAD-only: the
route (`_authorize_project_lead`) refuses a plain project_tasks worker.
It toggles only the `claimable` label (the fleet-pickup flag), preserving

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: claimable route description is ambiguous about label semantics

"It toggles only the claimable label" could be read as replacing all labels with ["claimable"] rather than adding/removing it in place. The unquarantine entry below it avoids this ambiguity by describing its behaviour explicitly ("clears its strikes"). Consider rephrasing to: "It adds or removes the claimable label while preserving every other label" to match the precision of the neighbouring bullet.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

every other label, so it does not widen the scope into free field edits
(cf. PATCH).
`POST .../tasks/{id}/unquarantine` is also reachable, but LEAD-only: the
route (`_authorize_project_lead`) refuses a plain project_tasks worker.
It returns a quarantined card to the open pool and clears its strikes.
Expand Down
21 changes: 21 additions & 0 deletions tests/test_task_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from tinyagentos.board_audit import BoardAuditLog
from tinyagentos.projects import task_store as task_store_mod
from tinyagentos.projects.task_store import ProjectTaskStore

Expand Down Expand Up @@ -599,3 +600,23 @@ async def test_no_broker_no_error(tmp_path):
await s.claim_task(task["id"], "worker-1")
await s.close_task(task["id"], "worker-1")
await s.close()


@pytest.mark.asyncio
async def test_quarantine_claimed_task_records_actual_from_status(tmp_path):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: New test only covers the claimed→quarantined path — no assertion for the open (unclaimed) path

The test exercises the previously-buggy claimed case but never quarantines an unclaimed open task. If the from_status ternary were accidentally inverted (e.g. "open" if existing.get("claimed_by") else "claimed"), the open-path audit record would silently break without any test catching it. Add a companion assertion that quarantining an unclaimed task records from_status == "open".


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

audit = BoardAuditLog(tmp_path / "audit.db")
await audit.init()
s = ProjectTaskStore(tmp_path / "tasks.db", audit=audit)
await s.init()
try:
task = await s.create_task("prj-1", "Task", "alice")
await s.claim_task(task["id"], "worker-1")
ok = await s.quarantine_task(task["id"], "system")
assert ok is True
history = await audit.history(task["id"])
quarantined = [h for h in history if h["event"] == "task.quarantined"]
assert len(quarantined) == 1
assert quarantined[0]["from_status"] == "claimed"
finally:
await s.close()
await audit.close()
7 changes: 6 additions & 1 deletion tinyagentos/projects/task_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,13 @@ async def quarantine_task(self, task_id: str, actor: str) -> bool:
"task.quarantined",
{"id": task_id, "actor": actor},
)
# Derive the pre-quarantine status race-free from the committed row
# rather than a separate pre-read (which would have a TOCTOU gap).
# quarantine does not clear claimed_by, so a set claimer means it was
# 'claimed' (cf. close_task's derivation).
from_status = "claimed" if existing and existing.get("claimed_by") else "open"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: from_status derivation is incomplete for non-claimed statuses

The ternary collapses every status except "claimed" to "open", but quarantine_task's UPDATE guard also allows status = 'reopened' tasks (only closed/cancelled/quarantined are excluded). If a reopened task is quarantined, the audit record will incorrectly show "open" → "quarantined" instead of "reopened" → "quarantined". The same two-value pattern exists in close_task (line 393), so this is at least partially pre-existing, but the new code replicates it without addressing it. Consider deriving from existing["status"] directly, or documenting the intentional simplification.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

await self._record_audit(
task_id, "task.quarantined", actor, "open", "quarantined",
task_id, "task.quarantined", actor, from_status, "quarantined",
project_id=existing["project_id"] if existing else "",
)
return changed
Expand Down
Loading