From a408a994e47a6c9f5857252c461bf8c039e42dd5 Mon Sep 17 00:00:00 2001 From: kami Date: Thu, 4 Jun 2026 02:00:23 +0400 Subject: [PATCH] =?UTF-8?q?feat(workflow):=20analyst=E2=86=92architect?= =?UTF-8?q?=E2=86=92planner=E2=86=92implementer=E2=87=84reviewer=20role=20?= =?UTF-8?q?pipeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/schemas/analysis.json | 19 +++ docs/schemas/design.json | 19 +++ docs/schemas/impl_plan.json | 15 +++ examples/workflows/role_pipeline.toml | 124 ++++++++++++++++++ .../workflow/RolePipelineWorkflowTest.kt | 73 +++++++++++ 5 files changed, 250 insertions(+) create mode 100644 docs/schemas/analysis.json create mode 100644 docs/schemas/design.json create mode 100644 docs/schemas/impl_plan.json create mode 100644 examples/workflows/role_pipeline.toml create mode 100644 infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/RolePipelineWorkflowTest.kt diff --git a/docs/schemas/analysis.json b/docs/schemas/analysis.json new file mode 100644 index 00000000..47f63184 --- /dev/null +++ b/docs/schemas/analysis.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "summary": { + "type": "string", + "description": "what the request is asking for, in your own words" + }, + "requirements": { + "type": "string", + "description": "concrete requirements / acceptance criteria, newline-separated" + }, + "affected_areas": { + "type": "string", + "description": "files, modules, or subsystems likely involved, newline-separated" + } + }, + "required": ["summary", "requirements"], + "additionalProperties": true +} diff --git a/docs/schemas/design.json b/docs/schemas/design.json new file mode 100644 index 00000000..c557f455 --- /dev/null +++ b/docs/schemas/design.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "properties": { + "approach": { + "type": "string", + "description": "the chosen approach and why" + }, + "components": { + "type": "string", + "description": "components/files to add or change, newline-separated" + }, + "risks": { + "type": "string", + "description": "risks, trade-offs, or open questions, newline-separated" + } + }, + "required": ["approach", "components"], + "additionalProperties": true +} diff --git a/docs/schemas/impl_plan.json b/docs/schemas/impl_plan.json new file mode 100644 index 00000000..ec5e6161 --- /dev/null +++ b/docs/schemas/impl_plan.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "properties": { + "steps": { + "type": "string", + "description": "ordered, individually verifiable implementation steps, newline-separated" + }, + "verification": { + "type": "string", + "description": "how completion is checked (commands, tests), newline-separated" + } + }, + "required": ["steps"], + "additionalProperties": true +} diff --git a/examples/workflows/role_pipeline.toml b/examples/workflows/role_pipeline.toml new file mode 100644 index 00000000..27fa607e --- /dev/null +++ b/examples/workflows/role_pipeline.toml @@ -0,0 +1,124 @@ +# Role pipeline: analyst → architect → planner → implementer ⇄ reviewer +# +# Each stage produces a typed artifact that the next stage `needs`, so work flows forward +# without a human relaying notes. The decision journal (pinned into every stage's context) +# carries steering/approvals/verdicts across the whole run, so the reviewer sees the same +# ground truth as the planner. +# +# The implementer⇄reviewer loop is gated by review_report.verdict: +# approved → done +# changes_requested → back to implementer (capped by implementer.max_retries, then escalates) +# +# Requires these artifact kinds in ~/.config/correx/config.toml (schemas under docs/schemas/): +# [[artifacts]] +# id = "analysis"; schema_path = "schemas/analysis.json"; llm_emitted = true +# [[artifacts]] +# id = "design"; schema_path = "schemas/design.json"; llm_emitted = true +# [[artifacts]] +# id = "impl_plan"; schema_path = "schemas/impl_plan.json"; llm_emitted = true +# [[artifacts]] +# id = "review_report"; schema_path = "schemas/review_report.json"; llm_emitted = true +# +# Prompt files (prompts/*.md, relative to this workflow) must exist for a real run. + +id = "role_pipeline" +start = "analyst" + +# 1. Understand the request and the relevant code. Read-only. +[[stages]] +id = "analyst" +prompt = "prompts/analyst.md" +produces = [{ name = "analysis", kind = "analysis" }] +allowed_tools = ["file_read", "ShellTool"] +token_budget = 16384 +max_retries = 2 + +# 2. Decide the approach and component boundaries. +[[stages]] +id = "architect" +prompt = "prompts/architect.md" +needs = ["analysis"] +produces = [{ name = "design", kind = "design" }] +token_budget = 16384 +max_retries = 2 + +# 3. Break the design into ordered, verifiable steps. +[[stages]] +id = "planner" +prompt = "prompts/planner.md" +needs = ["design"] +produces = [{ name = "impl_plan", kind = "impl_plan" }] +token_budget = 16384 +max_retries = 2 + +# 4. Implement the plan. Writes files (jailed to the workspace). The loop target — +# max_retries here caps how many review→implement refinement rounds are allowed. +[[stages]] +id = "implementer" +prompt = "prompts/implementer.md" +needs = ["impl_plan"] +produces = [{ name = "patch", kind = "file_written" }] +allowed_tools = ["file_read", "file_write", "file_edit", "ShellTool"] +token_budget = 32768 +max_retries = 3 + +# 5. Review the patch against the plan; emit a structured verdict. +[[stages]] +id = "reviewer" +prompt = "prompts/reviewer.md" +needs = ["patch", "impl_plan"] +produces = [{ name = "review_report", kind = "review_report" }] +token_budget = 32768 +max_retries = 2 + +# --- forward edges --- + +[[transitions]] +id = "analyst-to-architect" +from = "analyst" +to = "architect" +condition_type = "artifact_validated" +condition_artifact_id = "analysis" + +[[transitions]] +id = "architect-to-planner" +from = "architect" +to = "planner" +condition_type = "artifact_validated" +condition_artifact_id = "design" + +[[transitions]] +id = "planner-to-implementer" +from = "planner" +to = "implementer" +condition_type = "artifact_validated" +condition_artifact_id = "impl_plan" + +[[transitions]] +id = "implementer-to-reviewer" +from = "implementer" +to = "reviewer" +condition_type = "artifact_validated" +condition_artifact_id = "patch" + +# --- verdict-gated loop exit / re-entry --- + +[[transitions]] +id = "review-approved" +from = "reviewer" +to = "done" +condition_type = "artifact_field_equals" +condition_artifact_id = "review_report" +condition_field = "verdict" +condition_value = "approved" +condition_operator = "eq" + +[[transitions]] +id = "review-changes-requested" +from = "reviewer" +to = "implementer" +condition_type = "artifact_field_equals" +condition_artifact_id = "review_report" +condition_field = "verdict" +condition_value = "approved" +condition_operator = "neq" diff --git a/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/RolePipelineWorkflowTest.kt b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/RolePipelineWorkflowTest.kt new file mode 100644 index 00000000..17eaf553 --- /dev/null +++ b/infrastructure/workflow/src/test/kotlin/com/correx/infrastructure/workflow/RolePipelineWorkflowTest.kt @@ -0,0 +1,73 @@ +package com.correx.infrastructure.workflow + +import com.correx.core.artifacts.kind.ConfigArtifactKind +import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry +import com.correx.core.artifacts.kind.JsonSchema +import com.correx.core.artifacts.kind.JsonSchemaProperty +import com.correx.core.events.types.StageId +import com.correx.core.transitions.conditions.ArtifactFieldEquals +import com.correx.core.transitions.conditions.FieldOperator +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.Test +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class RolePipelineWorkflowTest { + + private fun llmKind(id: String) = ConfigArtifactKind( + id = id, + schema = JsonSchema( + type = "object", + properties = mapOf("verdict" to JsonSchemaProperty(type = "string")), + additionalProperties = true, + ), + llmEmitted = true, + ) + + private val registry = DefaultArtifactKindRegistry().apply { + register(llmKind("analysis")) + register(llmKind("design")) + register(llmKind("impl_plan")) + register(llmKind("review_report")) + } + + // Walk up from the working dir to the repo root so the test finds the shipped file + // regardless of which module dir Gradle runs it from. + private fun repoFile(rel: String): Path? { + var dir: Path? = Path.of(System.getProperty("user.dir")).toAbsolutePath() + while (dir != null) { + val candidate = dir.resolve(rel) + if (candidate.exists()) return candidate + dir = dir.parent + } + return null + } + + @Test + fun `shipped role_pipeline toml parses with the full role graph`() { + val path = repoFile("examples/workflows/role_pipeline.toml") + assumeTrue(path != null, "role_pipeline.toml not found from ${System.getProperty("user.dir")}") + + val graph = TomlWorkflowLoader(registry).load(path!!) + + assertEquals( + setOf("analyst", "architect", "planner", "implementer", "reviewer"), + graph.stages.keys.map { it.value }.toSet(), + ) + assertEquals(6, graph.transitions.size) + + // Forward chaining via needs. + assertTrue(graph.stages[StageId("reviewer")]!!.needs.map { it.value }.containsAll(listOf("patch", "impl_plan"))) + + // Verdict-gated loop: approved → done, changes → implementer. + val approved = graph.transitions.first { it.to == StageId("done") }.condition as ArtifactFieldEquals + assertEquals(FieldOperator.EQ, approved.operator) + assertEquals("verdict", approved.field) + val loop = graph.transitions + .first { it.from == StageId("reviewer") && it.to == StageId("implementer") } + .condition as ArtifactFieldEquals + assertEquals(FieldOperator.NEQ, loop.operator) + } +}