feat(tasks): per-task history / audit timeline

Surfaces a task's lifecycle straight from the event log (the source of truth) so
you can see what actually happened to it — claim/submit/complete, notes, links,
git-driven status — rather than only its current state. Useful for debugging a
live agent run, where current state alone doesn't say how it got there.

- TaskHistory.render: pure timeline renderer (split content/lifecycle to stay under
  the complexity cap, mirroring DefaultTaskReducer).
- TaskService.history(id): the task's own events, in order; survives soft-delete
  (the deletion is part of the trail), empty for an id that never existed.
- GET /tasks/{id}/history (200 text timeline, 404 when the id never existed) and
  the `correx task history <id>` CLI command.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 16:37:50 +00:00
parent a96c05e228
commit bce62c080c
7 changed files with 226 additions and 0 deletions
@@ -91,6 +91,7 @@ class TaskCommand : CliktCommand(name = "task") {
TaskListCommand(),
TaskSearchCommand(),
TaskShowCommand(),
TaskHistoryCommand(),
TaskCreateCommand(),
TaskClaimCommand(),
TaskCompleteCommand(),
@@ -160,6 +161,19 @@ class TaskShowCommand : TaskHttpCommand("show") {
}
}
class TaskHistoryCommand : TaskHttpCommand("history") {
private val id by argument("TASK_ID")
override fun run() = http { client ->
val resp = client.get("${baseUrl()}/tasks/$id/history")
if (resp.status == HttpStatusCode.NotFound) {
System.err.println("No such task: $id")
} else {
print(resp.bodyAsText())
}
}
}
class TaskCreateCommand : TaskHttpCommand("create") {
private val project by option("--project", help = "Project key, e.g. 'auth'").required()
private val title by option("--title").required()