summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 03:53:55 +0000
committerGitHub <noreply@github.com>2026-01-25 03:53:55 +0000
commitc908854e7e3571c99cce9f46497ce5337ea0aed1 (patch)
tree3d35137a452c562059ecd8c759393b90937df70c
parent03ab90836707954277597dc21fd8035019d8e221 (diff)
downloadsoryu-c908854e7e3571c99cce9f46497ce5337ea0aed1.tar.gz
soryu-c908854e7e3571c99cce9f46497ce5337ea0aed1.zip
Update create contract page to use dynamic templates (#29)
* feat: Add contract type templates API endpoint Add a new API endpoint to provide contract type templates (workflow definitions) separate from file templates. This enables the create contract page to dynamically show available contract types. Changes: - Add ContractTypeTemplate struct in templates.rs with id, name, description, phases, default_phase, and is_builtin fields - Add built-in contract types: 'simple' (plan/execute) and 'specification' (research/specify/plan/execute/review) - Add GET /api/v1/contract-types endpoint returning all contract types - Add frontend ContractTypeTemplate interface and listContractTypes() API function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: Update create contract modal to use dynamic templates - Add ContractTypeTemplate interface and listContractTypes API function to frontend api.ts - Add GET /api/v1/contract-types backend endpoint that returns built-in contract types (simple, specification) with their phases and defaults - Update create contract modal to fetch contract types dynamically when opened, with loading state and fallback to hardcoded types on error - Dynamically render contract type selection buttons from fetched types - Update phase dropdown to use phases from selected contract type - Replace static getValidPhases/getDefaultPhase calls with dynamic data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
-rw-r--r--makima/frontend/src/lib/api.ts73
-rw-r--r--makima/frontend/src/routes/contracts.tsx121
-rw-r--r--makima/src/llm/templates.rs65
-rw-r--r--makima/src/server/handlers/templates.rs101
-rw-r--r--makima/src/server/mod.rs2
5 files changed, 323 insertions, 39 deletions
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 76ee4d4..64ce591 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1507,6 +1507,43 @@ export function getDefaultPhase(contractType: ContractType): ContractPhase {
return "research";
}
+// =============================================================================
+// Contract Type Templates
+// =============================================================================
+
+/** Contract type template returned by the API */
+export interface ContractTypeTemplate {
+ /** Template identifier (e.g., "simple", "specification") */
+ id: string;
+ /** Display name */
+ name: string;
+ /** Description of the contract type workflow */
+ description: string;
+ /** Ordered list of phases for this contract type */
+ phases: ContractPhase[];
+ /** Default starting phase */
+ defaultPhase: ContractPhase;
+ /** Whether this is a built-in type (always available) */
+ isBuiltin: boolean;
+}
+
+/** Response from list contract types endpoint */
+export interface ListContractTypesResponse {
+ types: ContractTypeTemplate[];
+}
+
+/**
+ * List available contract types/templates.
+ * Returns built-in types (simple, specification) and any custom types.
+ */
+export async function listContractTypes(): Promise<ListContractTypesResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types`);
+ if (!res.ok) {
+ throw new Error(`Failed to list contract types: ${res.statusText}`);
+ }
+ return res.json();
+}
+
export interface ContractRepository {
id: string;
contractId: string;
@@ -2008,6 +2045,42 @@ export async function getTemplate(id: string): Promise<FileTemplate> {
}
// =============================================================================
+// Contract Type Templates (Workflow Definitions)
+// =============================================================================
+
+/** A contract type template defining a workflow */
+export interface ContractTypeTemplate {
+ /** Unique identifier (e.g., 'simple', 'specification', 'feature-development') */
+ id: string;
+ /** Display name */
+ name: string;
+ /** What this contract type is for */
+ description: string;
+ /** Ordered list of phases in the workflow */
+ phases: string[];
+ /** Starting phase */
+ defaultPhase: string;
+ /** True for built-in types ('simple', 'specification') */
+ isBuiltin: boolean;
+}
+
+export interface ListContractTypesResponse {
+ contractTypes: ContractTypeTemplate[];
+}
+
+/**
+ * List all available contract type templates.
+ * Returns built-in types (simple, specification) and any custom types.
+ */
+export async function listContractTypes(): Promise<ListContractTypesResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types`);
+ if (!res.ok) {
+ throw new Error(`Failed to list contract types: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
// Supervisor Question Types and Functions
// =============================================================================
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index 6946cb8..4f74692 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -6,7 +6,12 @@ import { ContractDetail } from "../components/contracts/ContractDetail";
import { DirectoryInput } from "../components/mesh/DirectoryInput";
import { useContracts } from "../hooks/useContracts";
import { useAuth } from "../contexts/AuthContext";
-import { createTask, getDaemonDirectories, getRepositorySuggestions } from "../lib/api";
+import {
+ createTask,
+ getDaemonDirectories,
+ getRepositorySuggestions,
+ listContractTypes,
+} from "../lib/api";
import type {
ContractWithRelations,
ContractSummary,
@@ -17,8 +22,8 @@ import type {
RepositorySourceType,
DaemonDirectory,
RepositoryHistoryEntry,
+ ContractTypeTemplate,
} from "../lib/api";
-import { getValidPhases, getDefaultPhase } from "../lib/api";
export default function ContractsPage() {
const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
@@ -85,6 +90,43 @@ function ContractsPageContent() {
const [suggestedDirectories, setSuggestedDirectories] = useState<DaemonDirectory[]>([]);
const [repoSuggestions, setRepoSuggestions] = useState<RepositoryHistoryEntry[]>([]);
const [showRepoSuggestions, setShowRepoSuggestions] = useState(false);
+ const [contractTypes, setContractTypes] = useState<ContractTypeTemplate[]>([]);
+ const [contractTypesLoading, setContractTypesLoading] = useState(false);
+
+ // Fetch contract types when modal opens
+ useEffect(() => {
+ if (isCreating) {
+ setContractTypesLoading(true);
+ listContractTypes()
+ .then((res) => {
+ setContractTypes(res.types);
+ setContractTypesLoading(false);
+ })
+ .catch((err) => {
+ console.error("Failed to fetch contract types:", err);
+ // Fall back to built-in types
+ setContractTypes([
+ {
+ id: "simple",
+ name: "Simple",
+ description: "Plan \u2192 Execute: Simple workflow with a plan document",
+ phases: ["plan", "execute"],
+ defaultPhase: "plan",
+ isBuiltin: true,
+ },
+ {
+ id: "specification",
+ name: "Specification",
+ description: "Research \u2192 Specify \u2192 Plan \u2192 Execute \u2192 Review: Full specification-driven development with TDD",
+ phases: ["research", "specify", "plan", "execute", "review"],
+ defaultPhase: "research",
+ isBuiltin: true,
+ },
+ ]);
+ setContractTypesLoading(false);
+ });
+ }
+ }, [isCreating]);
// Fetch repository suggestions when modal opens and repo type changes
useEffect(() => {
@@ -170,11 +212,15 @@ function ContractsPageContent() {
setCreateError(null);
+ // Get default phase from contract types or fall back to static function
+ const selectedType = contractTypes.find((t) => t.id === contractType);
+ const defaultPhaseForType = selectedType?.defaultPhase || (contractType === "simple" ? "plan" : "research");
+
const data: CreateContractRequest = {
name: newContractName.trim(),
description: newContractDescription.trim() || undefined,
contractType: contractType,
- initialPhase: initialPhase !== getDefaultPhase(contractType) ? initialPhase : undefined,
+ initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined,
};
try {
@@ -224,6 +270,7 @@ function ContractsPageContent() {
newContractName,
newContractDescription,
contractType,
+ contractTypes,
initialPhase,
repoType,
repoName,
@@ -516,41 +563,37 @@ function ContractsPageContent() {
<label className="block font-mono text-xs text-[#8b949e] uppercase mb-1">
Contract Type
</label>
- <div className="flex gap-2">
- <button
- type="button"
- onClick={() => {
- setContractType("simple");
- setInitialPhase("plan");
- }}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- contractType === "simple"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
- }`}
- >
- Simple
- </button>
- <button
- type="button"
- onClick={() => {
- setContractType("specification");
- setInitialPhase("research");
- }}
- className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
- contractType === "specification"
- ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
- : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
- }`}
- >
- Specification
- </button>
- </div>
- <p className="mt-1 font-mono text-xs text-[#8b949e]">
- {contractType === "simple"
- ? "Plan → Execute: Simple workflow with a plan document"
- : "Research → Specify → Plan → Execute → Review: Full specification-driven development with TDD"}
- </p>
+ {contractTypesLoading ? (
+ <div className="flex items-center justify-center py-4">
+ <span className="font-mono text-xs text-[#8b949e]">Loading contract types...</span>
+ </div>
+ ) : (
+ <>
+ <div className="flex gap-2">
+ {contractTypes.map((type) => (
+ <button
+ key={type.id}
+ type="button"
+ onClick={() => {
+ setContractType(type.id as ContractType);
+ setInitialPhase(type.defaultPhase as ContractPhase);
+ }}
+ className={`flex-1 px-3 py-2 font-mono text-xs uppercase transition-colors ${
+ contractType === type.id
+ ? "bg-[#0f3c78] text-[#dbe7ff] border border-[#75aafc]"
+ : "bg-[#0d1b2d] text-[#8b949e] border border-[#3f6fb3] hover:border-[#75aafc]"
+ }`}
+ >
+ {type.name}
+ </button>
+ ))}
+ </div>
+ <p className="mt-1 font-mono text-xs text-[#8b949e]">
+ {contractTypes.find((t) => t.id === contractType)?.description ||
+ "Select a contract type"}
+ </p>
+ </>
+ )}
</div>
{/* Starting Phase */}
@@ -563,7 +606,7 @@ function ContractsPageContent() {
onChange={(e) => setInitialPhase(e.target.value as ContractPhase)}
className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]"
>
- {getValidPhases(contractType).map((phase) => (
+ {(contractTypes.find((t) => t.id === contractType)?.phases || []).map((phase) => (
<option key={phase} value={phase}>
{phase.charAt(0).toUpperCase() + phase.slice(1)}
</option>
diff --git a/makima/src/llm/templates.rs b/makima/src/llm/templates.rs
index 18ef46d..7a5bd38 100644
--- a/makima/src/llm/templates.rs
+++ b/makima/src/llm/templates.rs
@@ -8,6 +8,71 @@ use utoipa::ToSchema;
use crate::db::models::BodyElement;
+// =============================================================================
+// Contract Type Templates (Workflow Definitions)
+// =============================================================================
+
+/// A contract type template defining a workflow
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ContractTypeTemplate {
+ /// Unique identifier (e.g., 'simple', 'specification', 'feature-development')
+ pub id: String,
+ /// Display name
+ pub name: String,
+ /// What this contract type is for
+ pub description: String,
+ /// Ordered list of phases in the workflow
+ pub phases: Vec<String>,
+ /// Starting phase
+ pub default_phase: String,
+ /// True for built-in types ('simple', 'specification')
+ pub is_builtin: bool,
+}
+
+/// Get all available contract type templates
+pub fn all_contract_types() -> Vec<ContractTypeTemplate> {
+ vec![
+ simple_contract_type(),
+ specification_contract_type(),
+ ]
+}
+
+/// Simple contract type with basic plan/execute workflow
+fn simple_contract_type() -> ContractTypeTemplate {
+ ContractTypeTemplate {
+ id: "simple".to_string(),
+ name: "Simple".to_string(),
+ description: "A basic workflow for straightforward tasks with planning and execution phases."
+ .to_string(),
+ phases: vec!["plan".to_string(), "execute".to_string()],
+ default_phase: "plan".to_string(),
+ is_builtin: true,
+ }
+}
+
+/// Specification contract type with full research-to-review workflow
+fn specification_contract_type() -> ContractTypeTemplate {
+ ContractTypeTemplate {
+ id: "specification".to_string(),
+ name: "Specification".to_string(),
+ description: "A comprehensive workflow for complex projects requiring research, specification, planning, execution, and review.".to_string(),
+ phases: vec![
+ "research".to_string(),
+ "specify".to_string(),
+ "plan".to_string(),
+ "execute".to_string(),
+ "review".to_string(),
+ ],
+ default_phase: "research".to_string(),
+ is_builtin: true,
+ }
+}
+
+// =============================================================================
+// File Templates
+// =============================================================================
+
/// A file template with suggested structure
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct FileTemplate {
diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs
index 868d5b4..5cad44f 100644
--- a/makima/src/server/handlers/templates.rs
+++ b/makima/src/server/handlers/templates.rs
@@ -5,6 +5,77 @@ use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::llm::templates;
+use crate::llm::templates::ContractTypeTemplate;
+
+// =============================================================================
+// Contract Type Templates
+// =============================================================================
+
+/// Contract type template for API response
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ContractTypeTemplate {
+ /// Template identifier (e.g., "simple", "specification")
+ pub id: String,
+ /// Display name
+ pub name: String,
+ /// Description of the contract type workflow
+ pub description: String,
+ /// Ordered list of phases for this contract type
+ pub phases: Vec<String>,
+ /// Default starting phase
+ pub default_phase: String,
+ /// Whether this is a built-in type (always available)
+ pub is_builtin: bool,
+}
+
+/// Response for listing contract types
+#[derive(Debug, Serialize, ToSchema)]
+pub struct ListContractTypesResponse {
+ pub types: Vec<ContractTypeTemplate>,
+}
+
+/// List available contract type templates
+#[utoipa::path(
+ get,
+ path = "/api/v1/contract-types",
+ responses(
+ (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse)
+ ),
+ tag = "contract-types"
+)]
+pub async fn list_contract_types() -> impl IntoResponse {
+ let types = vec![
+ ContractTypeTemplate {
+ id: "simple".to_string(),
+ name: "Simple".to_string(),
+ description: "Plan \u{2192} Execute: Simple workflow with a plan document".to_string(),
+ phases: vec!["plan".to_string(), "execute".to_string()],
+ default_phase: "plan".to_string(),
+ is_builtin: true,
+ },
+ ContractTypeTemplate {
+ id: "specification".to_string(),
+ name: "Specification".to_string(),
+ description: "Research \u{2192} Specify \u{2192} Plan \u{2192} Execute \u{2192} Review: Full specification-driven development with TDD".to_string(),
+ phases: vec![
+ "research".to_string(),
+ "specify".to_string(),
+ "plan".to_string(),
+ "execute".to_string(),
+ "review".to_string(),
+ ],
+ default_phase: "research".to_string(),
+ is_builtin: true,
+ },
+ ];
+
+ (
+ StatusCode::OK,
+ Json(ListContractTypesResponse { types }),
+ )
+ .into_response()
+}
/// Query parameters for listing templates
#[derive(Debug, Deserialize, ToSchema)]
@@ -105,3 +176,33 @@ pub async fn get_template(
.into_response(),
}
}
+
+// =============================================================================
+// Contract Type Templates (Workflow Definitions)
+// =============================================================================
+
+/// Response for listing contract types
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ListContractTypesResponse {
+ pub contract_types: Vec<ContractTypeTemplate>,
+}
+
+/// List all available contract type templates
+#[utoipa::path(
+ get,
+ path = "/api/v1/contract-types",
+ responses(
+ (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse)
+ ),
+ tag = "templates"
+)]
+pub async fn list_contract_types() -> impl IntoResponse {
+ let contract_types = templates::all_contract_types();
+
+ (
+ StatusCode::OK,
+ Json(ListContractTypesResponse { contract_types }),
+ )
+ .into_response()
+}
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index de20569..75f64c6 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -208,6 +208,8 @@ pub fn make_router(state: SharedState) -> Router {
// Template endpoints
.route("/templates", get(templates::list_templates))
.route("/templates/{id}", get(templates::get_template))
+ // Contract type templates (workflow definitions)
+ .route("/contract-types", get(templates::list_contract_types))
// Settings endpoints
.route(
"/settings/repository-history",