diff --git a/packages/workflow-dashboard/src/components/workflow-detail.tsx b/packages/workflow-dashboard/src/components/workflow-detail.tsx
index 8c59230..77d2530 100644
--- a/packages/workflow-dashboard/src/components/workflow-detail.tsx
+++ b/packages/workflow-dashboard/src/components/workflow-detail.tsx
@@ -2,6 +2,7 @@ import { useMemo, useRef, useState } from "react";
import type { WorkflowDetail as WorkflowDetailData, WorkflowRoleDescriptor } from "../api.ts";
import { getWorkflowDetail } from "../api.ts";
import { useFetch } from "../hooks.ts";
+import { Markdown } from "./markdown.tsx";
import { type NodeState, WorkflowGraph } from "./workflow-graph/index.ts";
type Props = {
@@ -187,18 +188,16 @@ function RoleCard({ roleName, role }: { roleName: string; role: WorkflowRoleDesc
>
System Prompt
-
- {role.systemPrompt}
-
+
+
)}
{rows.length > 0 && (
diff --git a/packages/workflow-template-develop/develop.esm.js b/packages/workflow-template-develop/develop.esm.js
new file mode 100644
index 0000000..b3d314b
--- /dev/null
+++ b/packages/workflow-template-develop/develop.esm.js
@@ -0,0 +1,318 @@
+// bundle-entry.ts
+import { createCursorAgent } from "@uncaged/workflow-agent-cursor";
+import { createWorkflow } from "@uncaged/workflow-runtime";
+import { optionalEnv, requireEnv } from "@uncaged/workflow-util";
+
+// src/moderator.ts
+import {
+ END,
+ START
+} from "@uncaged/workflow-runtime";
+function coderFinishedAllPlannedPhases(phases, coderCompletedPhases) {
+ if (phases.length === 0) {
+ return true;
+ }
+ const plannedHashes = new Set(phases.map((p) => p.hash));
+ const lastHash = phases[phases.length - 1].hash;
+ const explicit = new Set(coderCompletedPhases.filter((h) => plannedHashes.has(h)));
+ if (phases.every((p) => explicit.has(p.hash))) {
+ return true;
+ }
+ if (coderCompletedPhases.some((h) => h === lastHash)) {
+ return true;
+ }
+ return false;
+}
+var plannerAborted = {
+ name: "plannerAborted",
+ description: "The planner aborted due to insufficient information",
+ check: (ctx) => {
+ const plannerStep = ctx.steps.find((s) => s.role === "planner");
+ if (plannerStep === undefined) {
+ return false;
+ }
+ return plannerStep.meta.status === "aborted";
+ }
+};
+var allPhasesComplete = {
+ name: "allPhasesComplete",
+ description: "All planned phases have been completed by the coder",
+ check: (ctx) => {
+ const plannerStep = ctx.steps.find((s) => s.role === "planner");
+ if (plannerStep === undefined) {
+ return true;
+ }
+ const phases = plannerStep.meta.status === "planned" ? plannerStep.meta.phases : [];
+ if (!Array.isArray(phases)) {
+ return true;
+ }
+ const coderCompletedPhases = ctx.steps.filter((s) => s.role === "coder").map((s) => s.meta.completedPhase);
+ return coderFinishedAllPlannedPhases(phases, coderCompletedPhases);
+ }
+};
+var reviewApproved = {
+ name: "reviewApproved",
+ description: "The last reviewer approved the changes",
+ check: (ctx) => {
+ const last = ctx.steps[ctx.steps.length - 1];
+ return last.role === "reviewer" && last.meta.status === "approved";
+ }
+};
+var testsPassed = {
+ name: "testsPassed",
+ description: "The last tester reported tests passed",
+ check: (ctx) => {
+ const last = ctx.steps[ctx.steps.length - 1];
+ return last.role === "tester" && last.meta.status === "passed";
+ }
+};
+var table = {
+ [START]: [{ condition: "FALLBACK", role: "planner" }],
+ planner: [
+ { condition: plannerAborted, role: END },
+ { condition: "FALLBACK", role: "coder" }
+ ],
+ coder: [
+ { condition: allPhasesComplete, role: "reviewer" },
+ { condition: "FALLBACK", role: "coder" }
+ ],
+ reviewer: [
+ { condition: reviewApproved, role: "tester" },
+ { condition: "FALLBACK", role: "coder" }
+ ],
+ tester: [
+ { condition: testsPassed, role: "committer" },
+ { condition: "FALLBACK", role: "coder" }
+ ],
+ committer: [{ condition: "FALLBACK", role: END }]
+};
+
+// src/roles/coder.ts
+import * as z from "zod/v4";
+var coderMetaSchema = z.object({
+ completedPhase: z.string().describe("The planner phase hash finished this round. If multiple phases were completed, use the last finished phase hash."),
+ filesChanged: z.array(z.string()),
+ summary: z.string()
+});
+var CODER_SYSTEM = `You are a **coder**. Read the thread for the plan and work on the NEXT incomplete phase only.
+
+Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
+
+## Reading phase details
+
+Each planner phase has a content-hash and title. Read full details with \`uncaged-workflow cas get \`.
+
+The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
+
+## Completing a phase
+
+Report which phase you completed using the phase **hash** (not the title). If you legitimately finish every remaining phase in this single turn, set completedPhase to the **last** phase hash in the plan (the workflow treats that as full completion). List the files you changed and summarize what you did.
+
+## Output rules
+
+Keep your final response **short** — a brief summary paragraph plus the structured meta output. Do NOT paste diffs, file contents, or code blocks in your response. The actual changes are already on disk; repeating them wastes tokens. Just say what you did and why.`;
+var coderRole = {
+ description: "Implements the next incomplete planner phase and reports structured completion metadata.",
+ systemPrompt: CODER_SYSTEM,
+ schema: coderMetaSchema,
+ extractRefs: (meta) => [meta.completedPhase]
+};
+
+// src/roles/committer.ts
+import * as z2 from "zod/v4";
+var committerMetaSchema = z2.discriminatedUnion("status", [
+ z2.object({
+ status: z2.literal("committed"),
+ branch: z2.string(),
+ commitSha: z2.string()
+ }),
+ z2.object({
+ status: z2.literal("recoverable"),
+ error: z2.string(),
+ logRef: z2.string().nullable()
+ }),
+ z2.object({
+ status: z2.literal("unrecoverable"),
+ error: z2.string(),
+ logRef: z2.string().nullable()
+ })
+]);
+var COMMITTER_SYSTEM = `You are the git committer. Create a branch and commit the changes.
+Report the branch name and commit SHA. On failure, classify as recoverable or unrecoverable.
+Do not attempt to fix failures yourself.`;
+var committerRole = {
+ description: "Creates a branch and commits changes.",
+ systemPrompt: COMMITTER_SYSTEM,
+ schema: committerMetaSchema,
+ extractRefs: null
+};
+
+// src/roles/planner.ts
+import * as z3 from "zod/v4";
+var phaseSchema = z3.object({
+ hash: z3.string(),
+ title: z3.string()
+});
+var plannerMetaSchema = z3.discriminatedUnion("status", [
+ z3.object({
+ status: z3.literal("planned"),
+ phases: z3.array(phaseSchema)
+ }),
+ z3.object({
+ status: z3.literal("aborted"),
+ reason: z3.string().describe("Why the task cannot proceed")
+ })
+]);
+var PLANNER_SYSTEM = `You are a **planner** for a software task. Break the work into **sequential phases** the coder will execute one at a time. **Abort** if the prompt lacks critical information (e.g. no project/workspace path, ambiguous target repo).
+
+Run \`uncaged-workflow skill develop\` for thread ID lookup, CAS commands, and meta output guide.
+
+## Prerequisites — check FIRST
+
+The prompt MUST include an **absolute filesystem path** to the project workspace (e.g. \`/home/user/repos/my-project\`). If no workspace path is given and you cannot reliably infer one from context, **abort immediately** with a clear reason explaining what information is missing. Do NOT guess paths.
+
+## Storing phase details — MANDATORY
+
+For each phase, store its full detail text in CAS via \`uncaged-workflow cas put ''\`. The command prints a content-hash — use that as the phase identifier.
+
+The thread ID (26-char Crockford Base32) appears in the first message. If unsure, run \`uncaged-workflow thread list\`.
+
+**Do NOT store phase details in any other way** — the CLI is the only supported storage mechanism.
+
+## Phase granularity
+
+Match the number of phases to task complexity:
+- Trivial (add a config option, fix a typo, rename): 1 phase
+- Small (a new feature touching 2-3 files): 1-2 phases
+- Medium (cross-module refactor): 2-3 phases
+- Large (new subsystem, architectural change): 3-5 phases
+
+Fewer phases is always better. Each phase must justify its existence — if two phases would be tested together anyway, merge them.
+
+## Output format
+
+After storing all phases via the CLI, output compact JSON only:
+ { "status": "planned", "phases": [{ "hash": "", "title": "" }] }
+
+If aborting:
+ { "status": "aborted", "reason": "" }
+
+Order phases so earlier steps unblock later ones. Cover root cause, edge cases, and verification across the phases.
+
+## Output rules
+
+Keep your final response **short** — just the JSON with phases. Do NOT paste code snippets, diffs, or implementation details in your response. Phase details are already stored in CAS; your response should only contain the compact phases JSON.`;
+var plannerRole = {
+ description: "Breaks the task into sequential phases for the coder.",
+ systemPrompt: PLANNER_SYSTEM,
+ schema: plannerMetaSchema,
+ extractRefs: (meta) => meta.status === "planned" ? meta.phases.map((p) => p.hash) : []
+};
+
+// src/roles/reviewer.ts
+import * as z4 from "zod/v4";
+var reviewerMetaSchema = z4.discriminatedUnion("status", [
+ z4.object({
+ status: z4.literal("approved")
+ }),
+ z4.object({
+ status: z4.literal("rejected"),
+ issues: z4.array(z4.string()).describe("blocking issues that must be fixed")
+ })
+]);
+var REVIEWER_SYSTEM = `You are a code reviewer. Review the git diff for correctness, consistency, and adherence to project conventions.
+
+## Review process
+
+1. Read the **preparer**'s output in the thread for project conventions (coding style, naming, commit format, etc.).
+2. Review the diff against these conventions.
+3. For documentation changes, verify that names, paths, and references match the actual codebase.
+
+## Review checklist
+
+- **Correctness** — does the code do what it claims? Logic bugs, off-by-one, missing returns?
+- **Conventions** — naming, imports, code style per project rules?
+- **Consistency** — do docs/comments match actual code? Are references current and accurate?
+- **Edge cases** — missing error handling, null checks, boundary conditions?
+
+## Verdict
+
+- **Approve** only if there are zero issues
+- **Reject** with specific issues that must be fixed — every issue you find is blocking
+
+Be thorough. A false approve costs more than a false reject.
+
+## Output rules
+
+Keep your final response **short**. Summarize findings in a few bullet points, then output the structured verdict. Do NOT paste the full diff or large code blocks in your response.`;
+var reviewerRole = {
+ description: "Runs git diff checks and sets approved when the change is ready.",
+ systemPrompt: REVIEWER_SYSTEM,
+ schema: reviewerMetaSchema,
+ extractRefs: null
+};
+
+// src/roles/tester.ts
+import * as z5 from "zod/v4";
+var testerMetaSchema = z5.discriminatedUnion("status", [
+ z5.object({
+ status: z5.literal("passed"),
+ details: z5.string()
+ }),
+ z5.object({
+ status: z5.literal("failed"),
+ details: z5.string()
+ })
+]);
+var TESTER_SYSTEM = `You are a tester. Run the project's test suite, build, and lint commands. Check what commands are available from the preparer's output in the thread. Report pass/fail with details of what failed.
+
+## Output rules
+
+Keep your final response **short**. Report pass/fail with a brief summary of failures (if any). Do NOT paste full test output or build logs — just the key error lines.`;
+var testerRole = {
+ description: "Runs test, build, and lint commands and reports pass or fail with details.",
+ systemPrompt: TESTER_SYSTEM,
+ schema: testerMetaSchema,
+ extractRefs: null
+};
+
+// src/roles.ts
+var DEVELOP_WORKFLOW_DESCRIPTION = "Plan phases, implement incrementally, review, verify with tests/build/lint, and commit (planner → coder [repeat per phase] → reviewer → tester → committer).";
+var developRoles = {
+ planner: plannerRole,
+ coder: coderRole,
+ reviewer: reviewerRole,
+ tester: testerRole,
+ committer: committerRole
+};
+
+// src/descriptor.ts
+import { buildDescriptor } from "@uncaged/workflow-register";
+function buildDevelopDescriptor() {
+ return buildDescriptor({
+ description: DEVELOP_WORKFLOW_DESCRIPTION,
+ roles: developRoles,
+ table
+ });
+}
+// src/index.ts
+var developWorkflowDefinition = {
+ description: DEVELOP_WORKFLOW_DESCRIPTION,
+ roles: developRoles,
+ table
+};
+
+// bundle-entry.ts
+var adapter = createCursorAgent({
+ command: requireEnv("WORKFLOW_CURSOR_COMMAND", "set WORKFLOW_CURSOR_COMMAND (e.g. cursor-agent)"),
+ model: optionalEnv("WORKFLOW_CURSOR_MODEL"),
+ timeout: optionalEnv("WORKFLOW_CURSOR_TIMEOUT") ? Number(optionalEnv("WORKFLOW_CURSOR_TIMEOUT")) : 0,
+ workspace: null
+});
+var wf = createWorkflow(developWorkflowDefinition, { adapter, overrides: null });
+var descriptor = buildDevelopDescriptor();
+var run = wf;
+export {
+ run,
+ descriptor
+};