diff --git a/apps/backend/tests/integration/test_schema_storage_contracts.py b/apps/backend/tests/integration/test_schema_storage_contracts.py
index d345b36..5d29de6 100644
--- a/apps/backend/tests/integration/test_schema_storage_contracts.py
+++ b/apps/backend/tests/integration/test_schema_storage_contracts.py
@@ -75,6 +75,18 @@ class SchemaStorageContractsIntegrationTest(unittest.TestCase):
"transform_script",
}.issubset(repository.schema.list_columns("script_config_versions"))
)
+ self.assertTrue(
+ {
+ "parent_job_id",
+ "attempt",
+ "stdout",
+ "stderr",
+ "exit_code",
+ "duration_ms",
+ "workspace_path",
+ "output_files",
+ }.issubset(repository.schema.list_columns("agent_jobs"))
+ )
self.assertTrue(
{
"s3_prefix",
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 41b8229..932f6cf 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -6,7 +6,7 @@
"build": "next build",
"start": "next start",
"test:roles": "node tests/role-navigation.test.mjs",
- "test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs",
+ "test:ui": "node tests/role-navigation.test.mjs && node tests/article_dashboard.test.mjs && node tests/article_form_validation.test.mjs && node tests/admin_script_versions.model.test.mjs && node tests/agent_jobs.model.test.mjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
diff --git a/apps/frontend/src/app/admin/jobs/page.tsx b/apps/frontend/src/app/admin/jobs/page.tsx
new file mode 100644
index 0000000..f99ac85
--- /dev/null
+++ b/apps/frontend/src/app/admin/jobs/page.tsx
@@ -0,0 +1,3 @@
+import AdminJobsPage from "@/pages/admin-jobs";
+
+export default AdminJobsPage;
diff --git a/apps/frontend/src/features/agent-jobs/model.ts b/apps/frontend/src/features/agent-jobs/model.ts
new file mode 100644
index 0000000..a859a3d
--- /dev/null
+++ b/apps/frontend/src/features/agent-jobs/model.ts
@@ -0,0 +1,33 @@
+import type { AgentJobSummary } from "@pipeline/shared";
+
+export type AgentJobRow = {
+ id: string;
+ type: string;
+ status: string;
+ attempt: number;
+ parentJobId: string | null;
+ workspacePath: string | null;
+ stdout: string;
+ stderr: string;
+ exitCode: number | null;
+ durationMs: number | null;
+ outputFiles: string;
+ error: string;
+};
+
+export function buildAgentJobRows(jobs: readonly AgentJobSummary[]): AgentJobRow[] {
+ return jobs.map((job) => ({
+ id: job.id,
+ type: job.job_type,
+ status: job.status,
+ attempt: job.attempt,
+ parentJobId: job.parent_job_id ?? null,
+ workspacePath: job.workspace_path ?? null,
+ stdout: job.stdout,
+ stderr: job.stderr,
+ exitCode: job.exit_code ?? null,
+ durationMs: job.duration_ms ?? null,
+ outputFiles: job.output_files.map((file) => file.path).join(", "),
+ error: job.error_category ?? job.error_message ?? "",
+ }));
+}
diff --git a/apps/frontend/src/features/agent-jobs/ui.tsx b/apps/frontend/src/features/agent-jobs/ui.tsx
new file mode 100644
index 0000000..4a33dcb
--- /dev/null
+++ b/apps/frontend/src/features/agent-jobs/ui.tsx
@@ -0,0 +1,51 @@
+import type { AgentJobRow } from "./model";
+
+type AgentJobsTableProps = {
+ rows: readonly AgentJobRow[];
+};
+
+export function AgentJobsTable({ rows }: AgentJobsTableProps) {
+ if (rows.length === 0) {
+ return
No agent jobs yet.
;
+ }
+
+ return (
+
+
+
+ | Job |
+ Status |
+ Attempt |
+ Runtime |
+ Workspace |
+ Logs |
+ Outputs |
+
+
+
+ {rows.map((row) => (
+
+ |
+ {row.type}
+ {row.id}
+ {row.parentJobId ? retry of {row.parentJobId} : null}
+ |
+ {row.status} |
+ {row.attempt} |
+
+ exit: {row.exitCode ?? "pending"}
+ {row.durationMs ?? 0} ms
+ {row.error ? {row.error} : null}
+ |
+ {row.workspacePath ?? "pending"} |
+
+ {row.stdout || "stdout pending"}
+ {row.stderr ? {row.stderr} : null}
+ |
+ {row.outputFiles || "none"} |
+
+ ))}
+
+
+ );
+}
diff --git a/apps/frontend/src/pages/admin-jobs/index.tsx b/apps/frontend/src/pages/admin-jobs/index.tsx
new file mode 100644
index 0000000..11f2178
--- /dev/null
+++ b/apps/frontend/src/pages/admin-jobs/index.tsx
@@ -0,0 +1,28 @@
+import Link from "next/link";
+
+import { AgentJobsTable } from "@/features/agent-jobs/ui";
+import { buildAgentJobRows } from "@/features/agent-jobs/model";
+import { fetchAgentJobs } from "@/shared/pipeline-api";
+import { RoleNavigation } from "@/widgets/role-navigation";
+
+export default async function AdminJobsPage() {
+ const jobs = await fetchAgentJobs();
+ const rows = buildAgentJobRows(jobs.jobs);
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/apps/frontend/src/shared/pipeline-api.ts b/apps/frontend/src/shared/pipeline-api.ts
index 127234a..1f8393b 100644
--- a/apps/frontend/src/shared/pipeline-api.ts
+++ b/apps/frontend/src/shared/pipeline-api.ts
@@ -2,6 +2,9 @@ import type {
ArticleCreateRequest,
ArticleCreateResponse,
ArticleDetailResponse,
+ AgentJobListResponse,
+ AgentJobResponse,
+ AgentJobTestCodexRequest,
ArticleListResponse,
CurrentUserResponse,
ScriptConfigVersionCreateRequest,
@@ -149,6 +152,45 @@ export function fetchCurrentUser(): Promise {
return apiGet("/api/me");
}
+export function fetchAgentJobs(
+ userEmail = DEMO_ADMIN_EMAIL,
+): Promise {
+ return apiGet("/api/agent-jobs", { userEmail });
+}
+
+export function createTestCodexJob(
+ request: AgentJobTestCodexRequest = { fake_result: "valid" },
+ userEmail = DEMO_ADMIN_EMAIL,
+): Promise {
+ return apiPost(
+ "/api/agent-jobs/test-codex",
+ request,
+ { userEmail },
+ );
+}
+
+export function retryAgentJob(
+ jobId: string,
+ userEmail = DEMO_ADMIN_EMAIL,
+): Promise {
+ return apiPost(
+ `/api/agent-jobs/${jobId}/retry`,
+ undefined,
+ { userEmail },
+ );
+}
+
+export function cancelAgentJob(
+ jobId: string,
+ userEmail = DEMO_ADMIN_EMAIL,
+): Promise {
+ return apiPost(
+ `/api/agent-jobs/${jobId}/cancel`,
+ undefined,
+ { userEmail },
+ );
+}
+
export function fetchTargetSites(): Promise {
return apiGet("/api/sites");
}
diff --git a/apps/frontend/src/widgets/role-navigation/model.ts b/apps/frontend/src/widgets/role-navigation/model.ts
index 83f4fb6..d2b5e85 100644
--- a/apps/frontend/src/widgets/role-navigation/model.ts
+++ b/apps/frontend/src/widgets/role-navigation/model.ts
@@ -38,6 +38,12 @@ export const ROLE_NAVIGATION_ITEMS = [
href: "/admin/scripts",
roles: ["ADMIN"],
},
+ {
+ id: "agent-jobs",
+ label: "Agent jobs",
+ href: "/admin/jobs",
+ roles: ["ADMIN"],
+ },
] as const satisfies readonly NavigationItem[];
export function getRoleNavigationItems(role: Role): NavigationItem[] {
diff --git a/apps/frontend/tests/agent_jobs.model.test.mjs b/apps/frontend/tests/agent_jobs.model.test.mjs
new file mode 100644
index 0000000..b7fcb12
--- /dev/null
+++ b/apps/frontend/tests/agent_jobs.model.test.mjs
@@ -0,0 +1,49 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+import ts from "typescript";
+
+const testDir = path.dirname(fileURLToPath(import.meta.url));
+const sourcePath = path.resolve(testDir, "../src/features/agent-jobs/model.ts");
+const source = readFileSync(sourcePath, "utf8");
+const compiled = ts.transpileModule(source, {
+ compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
+});
+
+const moduleExports = {};
+new Function("exports", compiled.outputText)(moduleExports);
+
+const { buildAgentJobRows } = moduleExports;
+
+const rows = buildAgentJobRows([
+ {
+ id: "11111111-1111-1111-1111-111111111111",
+ article_id: null,
+ parent_job_id: "00000000-0000-0000-0000-000000000000",
+ attempt: 2,
+ job_type: "TEST_CODEX",
+ agent_profile: "fake-codex",
+ status: "SUCCEEDED",
+ workspace_path: "/tmp/job",
+ input_files: [{ path: "inputs/job.json" }],
+ output_files: [{ path: "outputs/result.json" }],
+ error_category: null,
+ error_message: null,
+ stdout: "done",
+ stderr: "",
+ exit_code: 0,
+ duration_ms: 5,
+ queued_at: "2026-05-21T00:00:00Z",
+ started_at: "2026-05-21T00:00:01Z",
+ finished_at: "2026-05-21T00:00:02Z",
+ },
+]);
+
+assert.equal(rows.length, 1);
+assert.equal(rows[0].status, "SUCCEEDED");
+assert.equal(rows[0].attempt, 2);
+assert.equal(rows[0].parentJobId, "00000000-0000-0000-0000-000000000000");
+assert.equal(rows[0].outputFiles, "outputs/result.json");
+assert.equal(rows[0].stdout, "done");
diff --git a/apps/frontend/tests/role-navigation.test.mjs b/apps/frontend/tests/role-navigation.test.mjs
index 5521882..cde1428 100644
--- a/apps/frontend/tests/role-navigation.test.mjs
+++ b/apps/frontend/tests/role-navigation.test.mjs
@@ -36,5 +36,9 @@ assert.equal(
adminItems.some((item) => item.id === "script-versions"),
true,
);
+assert.equal(
+ adminItems.some((item) => item.id === "agent-jobs"),
+ true,
+);
console.log("role navigation hides Admin-only items for Editors");
diff --git a/apps/runner/src/application/fake_runner.py b/apps/runner/src/application/fake_runner.py
index eea0f35..8446bed 100644
--- a/apps/runner/src/application/fake_runner.py
+++ b/apps/runner/src/application/fake_runner.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import json
+import os
import shutil
import time
from pathlib import Path
@@ -90,6 +91,34 @@ def run_fake_runner_once(
)
+def run_fake_runner_loop(
+ *,
+ backend_url: str,
+ workspace_root: Path,
+ poll_interval_seconds: float,
+) -> None:
+ backend_client = HttpBackendJobClient(backend_url)
+ while True:
+ try:
+ run_fake_runner_once(backend_client, workspace_root=workspace_root)
+ except Exception:
+ pass
+ time.sleep(poll_interval_seconds)
+
+
+def get_backend_url() -> str:
+ backend_url = os.environ.get("BACKEND_URL")
+ if not backend_url:
+ raise RuntimeError("BACKEND_URL is required for fake runner")
+ return backend_url
+
+
+def get_workspace_root() -> Path:
+ return Path(
+ os.environ.get("RUNNER_WORKSPACE_ROOT", "/tmp/pipeline-runner-workspaces")
+ )
+
+
def materialize_fake_workspace(
job: dict[str, Any],
*,
diff --git a/apps/runner/src/presentation/main.py b/apps/runner/src/presentation/main.py
index 41ebbfb..30f0b4e 100644
--- a/apps/runner/src/presentation/main.py
+++ b/apps/runner/src/presentation/main.py
@@ -6,7 +6,13 @@ import threading
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
-from src.application.fake_runner import get_backend_url, get_workspace_root, run_fake_runner_loop
+from src.application.fake_runner import (
+ HttpBackendJobClient,
+ get_backend_url,
+ get_workspace_root,
+ run_fake_runner_loop,
+ run_fake_runner_once,
+)
class HealthHandler(BaseHTTPRequestHandler):
@@ -22,6 +28,32 @@ class HealthHandler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body)
+ def do_POST(self) -> None:
+ if self.path != "/run-once":
+ self.send_error(HTTPStatus.NOT_FOUND)
+ return
+
+ try:
+ result = run_fake_runner_once(
+ HttpBackendJobClient(get_backend_url()),
+ workspace_root=get_workspace_root(),
+ )
+ except Exception as error:
+ body = json.dumps({"status": "error", "detail": str(error)}).encode("utf-8")
+ self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+ return
+
+ body = json.dumps({"status": "ok", "job": result}, default=str).encode("utf-8")
+ self.send_response(HTTPStatus.OK)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
def log_message(self, format: str, *args: object) -> None:
return
diff --git a/apps/runner/tests/test_fake_runner.py b/apps/runner/tests/test_fake_runner.py
new file mode 100644
index 0000000..0dfbef9
--- /dev/null
+++ b/apps/runner/tests/test_fake_runner.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+import tempfile
+import sys
+import unittest
+from pathlib import Path
+from typing import Any
+
+
+REPO_ROOT = Path(__file__).resolve().parents[3]
+RUNNER_ROOT = REPO_ROOT / "apps" / "runner"
+sys.path.insert(0, str(RUNNER_ROOT))
+
+from src.application.fake_runner import run_fake_runner_once
+
+
+class FakeBackendJobClient:
+ def __init__(self, job: dict[str, Any] | None) -> None:
+ self.job = job
+ self.completed: dict[str, Any] | None = None
+
+ def claim_job(self) -> dict[str, Any] | None:
+ job = self.job
+ self.job = None
+ return job
+
+ def complete_job(self, job_id: str, payload: dict[str, Any]) -> dict[str, Any]:
+ self.completed = {"id": job_id, **payload}
+ return self.completed
+
+
+class FakeRunnerTest(unittest.TestCase):
+ def test_fake_runner_claims_job_writes_workspace_and_completes(self) -> None:
+ backend = FakeBackendJobClient(
+ {
+ "id": "11111111-1111-1111-1111-111111111111",
+ "agent_profile": "fake-codex",
+ }
+ )
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ result = run_fake_runner_once(
+ backend,
+ workspace_root=Path(temp_dir),
+ )
+ self.assertIsNotNone(result)
+ assert result is not None
+ workspace_path = Path(result["workspace_path"])
+ self.assertTrue((workspace_path / "inputs" / "job.json").is_file())
+ self.assertTrue((workspace_path / "logs" / "stdout.log").is_file())
+ self.assertTrue((workspace_path / "logs" / "stderr.log").is_file())
+ self.assertTrue((workspace_path / "outputs" / "result.json").is_file())
+ self.assertEqual(0, result["exit_code"])
+ self.assertEqual("SUCCEEDED", result["output"]["status"])
+ self.assertEqual([{"path": "outputs/result.json"}], result["output"]["output_files"])
+
+ def test_fake_runner_can_emit_invalid_schema_fixture(self) -> None:
+ backend = FakeBackendJobClient(
+ {
+ "id": "22222222-2222-2222-2222-222222222222",
+ "agent_profile": "fake-invalid-schema",
+ }
+ )
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ result = run_fake_runner_once(
+ backend,
+ workspace_root=Path(temp_dir),
+ )
+
+ self.assertIsNotNone(result)
+ assert result is not None
+ self.assertEqual({"status": "NOT_A_STATUS"}, result["output"])
+
+ def test_fake_runner_noops_without_queued_job(self) -> None:
+ backend = FakeBackendJobClient(None)
+
+ with tempfile.TemporaryDirectory() as temp_dir:
+ result = run_fake_runner_once(
+ backend,
+ workspace_root=Path(temp_dir),
+ )
+
+ self.assertIsNone(result)
+ self.assertIsNone(backend.completed)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/docker-compose.yml b/docker-compose.yml
index 17c54f6..18dd705 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,8 +41,12 @@ services:
dockerfile: apps/runner/Dockerfile
environment:
RUNNER_MODE: fake
+ BACKEND_URL: http://backend:8000
ports:
- "${RUNNER_PORT:-8010}:8010"
+ depends_on:
+ backend:
+ condition: service_started
postgres:
image: postgres:16-alpine
diff --git a/packages/shared/openapi.json b/packages/shared/openapi.json
index 3fc44b3..3ebcd10 100644
--- a/packages/shared/openapi.json
+++ b/packages/shared/openapi.json
@@ -19,6 +19,23 @@
"title": "AgentJobErrorCategory",
"type": "string"
},
+ "AgentJobListResponse": {
+ "additionalProperties": false,
+ "properties": {
+ "jobs": {
+ "items": {
+ "$ref": "#/components/schemas/AgentJobSummary"
+ },
+ "title": "Jobs",
+ "type": "array"
+ }
+ },
+ "required": [
+ "jobs"
+ ],
+ "title": "AgentJobListResponse",
+ "type": "object"
+ },
"AgentJobOutput": {
"additionalProperties": false,
"properties": {
@@ -74,6 +91,19 @@
"title": "AgentJobOutput",
"type": "object"
},
+ "AgentJobResponse": {
+ "additionalProperties": false,
+ "properties": {
+ "job": {
+ "$ref": "#/components/schemas/AgentJobSummary"
+ }
+ },
+ "required": [
+ "job"
+ ],
+ "title": "AgentJobResponse",
+ "type": "object"
+ },
"AgentJobStatus": {
"enum": [
"QUEUED",
@@ -94,9 +124,36 @@
"type": "string"
},
"article_id": {
- "format": "uuid",
- "title": "Article Id",
- "type": "string"
+ "anyOf": [
+ {
+ "format": "uuid",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Article Id"
+ },
+ "attempt": {
+ "default": 1,
+ "minimum": 1,
+ "title": "Attempt",
+ "type": "integer"
+ },
+ "duration_ms": {
+ "anyOf": [
+ {
+ "minimum": 0,
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Duration Ms"
},
"error_category": {
"anyOf": [
@@ -121,6 +178,18 @@
"default": null,
"title": "Error Message"
},
+ "exit_code": {
+ "anyOf": [
+ {
+ "type": "integer"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Exit Code"
+ },
"finished_at": {
"anyOf": [
{
@@ -156,6 +225,24 @@
"title": "Output Files",
"type": "array"
},
+ "parent_job_id": {
+ "anyOf": [
+ {
+ "format": "uuid",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Parent Job Id"
+ },
+ "queued_at": {
+ "format": "date-time",
+ "title": "Queued At",
+ "type": "string"
+ },
"started_at": {
"anyOf": [
{
@@ -172,6 +259,16 @@
"status": {
"$ref": "#/components/schemas/AgentJobStatus"
},
+ "stderr": {
+ "default": "",
+ "title": "Stderr",
+ "type": "string"
+ },
+ "stdout": {
+ "default": "",
+ "title": "Stdout",
+ "type": "string"
+ },
"workspace_path": {
"anyOf": [
{
@@ -187,14 +284,27 @@
},
"required": [
"id",
- "article_id",
"job_type",
"agent_profile",
- "status"
+ "status",
+ "queued_at"
],
"title": "AgentJobSummary",
"type": "object"
},
+ "AgentJobTestCodexRequest": {
+ "additionalProperties": false,
+ "properties": {
+ "fake_result": {
+ "default": "valid",
+ "pattern": "^(valid|invalid_schema)$",
+ "title": "Fake Result",
+ "type": "string"
+ }
+ },
+ "title": "AgentJobTestCodexRequest",
+ "type": "object"
+ },
"AgentJobType": {
"enum": [
"BOUNDARY_QUESTIONS",
@@ -1563,16 +1673,6 @@
"ScriptConfigVersionSummary": {
"additionalProperties": false,
"properties": {
- "created_at": {
- "format": "date-time",
- "title": "Created At",
- "type": "string"
- },
- "created_by": {
- "format": "uuid",
- "title": "Created By",
- "type": "string"
- },
"activated_at": {
"anyOf": [
{
@@ -1583,8 +1683,19 @@
"type": "null"
}
],
+ "default": null,
"title": "Activated At"
},
+ "created_at": {
+ "format": "date-time",
+ "title": "Created At",
+ "type": "string"
+ },
+ "created_by": {
+ "format": "uuid",
+ "title": "Created By",
+ "type": "string"
+ },
"diff": {
"additionalProperties": true,
"title": "Diff",
@@ -1600,14 +1711,6 @@
"title": "Publishing Yaml Hash",
"type": "string"
},
- "status": {
- "$ref": "#/components/schemas/ScriptConfigVersionStatus"
- },
- "target_site_id": {
- "format": "uuid",
- "title": "Target Site Id",
- "type": "string"
- },
"rollback_target_version_id": {
"anyOf": [
{
@@ -1618,8 +1721,17 @@
"type": "null"
}
],
+ "default": null,
"title": "Rollback Target Version Id"
},
+ "status": {
+ "$ref": "#/components/schemas/ScriptConfigVersionStatus"
+ },
+ "target_site_id": {
+ "format": "uuid",
+ "title": "Target Site Id",
+ "type": "string"
+ },
"transform_script_hash": {
"minLength": 1,
"title": "Transform Script Hash",
@@ -2116,6 +2228,290 @@
},
"openapi": "3.1.0",
"paths": {
+ "/api/agent-jobs": {
+ "get": {
+ "operationId": "get_agent_jobs_api_agent_jobs_get",
+ "parameters": [
+ {
+ "in": "header",
+ "name": "X-Demo-User-Email",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Demo-User-Email"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobListResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Get Agent Jobs",
+ "tags": [
+ "agent-jobs"
+ ]
+ }
+ },
+ "/api/agent-jobs/test-codex": {
+ "post": {
+ "operationId": "post_test_codex_job_api_agent_jobs_test_codex_post",
+ "parameters": [
+ {
+ "in": "header",
+ "name": "X-Demo-User-Email",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Demo-User-Email"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobTestCodexRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Post Test Codex Job",
+ "tags": [
+ "agent-jobs"
+ ]
+ }
+ },
+ "/api/agent-jobs/{job_id}": {
+ "get": {
+ "operationId": "get_agent_job_route_api_agent_jobs__job_id__get",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "job_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Job Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Demo-User-Email",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Demo-User-Email"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Get Agent Job Route",
+ "tags": [
+ "agent-jobs"
+ ]
+ }
+ },
+ "/api/agent-jobs/{job_id}/cancel": {
+ "post": {
+ "operationId": "post_cancel_agent_job_api_agent_jobs__job_id__cancel_post",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "job_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Job Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Demo-User-Email",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Demo-User-Email"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Post Cancel Agent Job",
+ "tags": [
+ "agent-jobs"
+ ]
+ }
+ },
+ "/api/agent-jobs/{job_id}/retry": {
+ "post": {
+ "operationId": "post_retry_agent_job_api_agent_jobs__job_id__retry_post",
+ "parameters": [
+ {
+ "in": "path",
+ "name": "job_id",
+ "required": true,
+ "schema": {
+ "format": "uuid",
+ "title": "Job Id",
+ "type": "string"
+ }
+ },
+ {
+ "in": "header",
+ "name": "X-Demo-User-Email",
+ "required": false,
+ "schema": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "X-Demo-User-Email"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentJobResponse"
+ }
+ }
+ },
+ "description": "Successful Response"
+ },
+ "422": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ "description": "Validation Error"
+ }
+ },
+ "summary": "Post Retry Agent Job",
+ "tags": [
+ "agent-jobs"
+ ]
+ }
+ },
"/api/articles": {
"get": {
"operationId": "list_articles_route_api_articles_get",
diff --git a/packages/shared/src/api-types.ts b/packages/shared/src/api-types.ts
index 3c16450..9854aef 100644
--- a/packages/shared/src/api-types.ts
+++ b/packages/shared/src/api-types.ts
@@ -3,6 +3,10 @@
export type AgentJobErrorCategory = "FAILED_SCHEMA_VALIDATION" | "CLI_EXIT_CODE_FAILURE" | "TIMEOUT" | "MISSING_OUTPUT_FILE" | "UNSUPPORTED_CLAIMS_FOUND" | "SOURCE_RETRIEVAL_FAILED" | "RESEARCH_ARTIFACT_UPLOAD_FAILED" | "PUBLISH_DRY_RUN_FAILED" | "GIT_CHECKOUT_FAILED" | "GIT_COMMIT_FAILED" | "GIT_PUSH_NON_FAST_FORWARD" | "PUBLISH_VERIFICATION_FAILED";
+export type AgentJobListResponse = {
+ jobs: AgentJobSummary[];
+};
+
export type AgentJobOutput = {
artifacts?: ResearchArtifactSummary[];
error_category?: AgentJobErrorCategory | null;
@@ -12,23 +16,38 @@ export type AgentJobOutput = {
status: AgentJobStatus;
};
+export type AgentJobResponse = {
+ job: AgentJobSummary;
+};
+
export type AgentJobStatus = "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "CANCELLED";
export type AgentJobSummary = {
agent_profile: string;
- article_id: string;
+ article_id?: string | null;
+ attempt?: number;
+ duration_ms?: number | null;
error_category?: AgentJobErrorCategory | null;
error_message?: string | null;
+ exit_code?: number | null;
finished_at?: string | null;
id: string;
input_files?: RunnerFileRef[];
job_type: AgentJobType;
output_files?: RunnerFileRef[];
+ parent_job_id?: string | null;
+ queued_at: string;
started_at?: string | null;
status: AgentJobStatus;
+ stderr?: string;
+ stdout?: string;
workspace_path?: string | null;
};
+export type AgentJobTestCodexRequest = {
+ fake_result?: string;
+};
+
export type AgentJobType = "BOUNDARY_QUESTIONS" | "PLAN_GENERATION" | "RESEARCH" | "EVIDENCE_MATRIX" | "SECTION_SCAFFOLD" | "DRAFT_ASSEMBLY" | "SEO_REVIEW" | "LANGUAGE_REVIEW" | "PUBLISH_DRY_RUN" | "PUBLISH_COMMIT" | "TEST_CODEX";
export type ArticleCreateRequest = {
@@ -266,15 +285,15 @@ export type ScriptConfigVersionResponse = {
export type ScriptConfigVersionStatus = "DRAFT" | "ACTIVE" | "DEPRECATED";
export type ScriptConfigVersionSummary = {
+ activated_at?: string | null;
created_at: string;
created_by: string;
- activated_at?: string | null;
- diff: Record;
+ diff?: Record;
id: string;
publishing_yaml_hash: string;
+ rollback_target_version_id?: string | null;
status: ScriptConfigVersionStatus;
target_site_id: string;
- rollback_target_version_id?: string | null;
transform_script_hash: string;
version: number;
};