feat(workflow): analyst→architect→planner→implementer⇄reviewer role pipeline
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
+73
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user