diff --git a/README.md b/README.md
index 75a99c96..b1799355 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ Our documentation website is the best place to find comprehensive information, t
| π§ **[Memory & Context](./docs/content/docs/core/memory/overview.mdx)** | Managing agent memory and conversation context |
| π **[MCP Integration](./docs/content/docs/core/mcp/overview.mdx)** | Model Context Protocol for external tool servers |
| π‘ **[Signal System](./docs/content/docs/core/signals/overview.mdx)** | Event-driven communication between components |
-| π **[Deployment](./docs/content/docs/core/deployment/docker.mdx)** | Deploy Compozy to production environments |
+| π **[Deployment](./docs/content/docs/deployment/docker.mdx)** | Deploy Compozy to production environments |
| π» **[CLI Reference](./docs/content/docs/cli/overview.mdx)** | Command-line interface reference |
| π **[Schema Definition](./docs/content/docs/schema/project.mdx)** | YAML schema definitions for all components |
| π **[API Reference](./docs/content/docs/api/overview.mdx)** | REST API for programmatic access |
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..8c7fe41d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Accepted Risks
+
+### AWS SDK for Go (github.com/aws/aws-sdk-go v1.55.6)
+
+- Advisories: GO-2022-0635, GO-2022-0646 (AWS S3 Crypto client-side encryption flaws)
+- Impact Scope: affects packages `s3crypto`/`s3/encryption`; Compozy does not invoke these modules.
+- Mitigation: storage interactions rely on server-side encryption; no client-side encryption helpers are linked into binaries.
+- Verification: `rg "s3crypto" -n` and `rg "s3/encryption" -n` return no matches in the repository (checked 2025-10-27).
+- Action: continue monitoring upstream advisories; upgrade or disable the dependency if future releases require the S3 Crypto helpers.
diff --git a/ai-docs/reviews-pr-305/issues/011-issue.md b/ai-docs/reviews-pr-305/issues/011-issue.md
new file mode 100644
index 00000000..53d8b8d9
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/011-issue.md
@@ -0,0 +1,49 @@
+# Issue 11 - Review Thread Comment
+
+**File:** `examples/temporal-standalone/integration-testing/tests/integration_test.go:44`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_π§Ή Nitpick_ | _π΅ Trivial_
+
+**Option: wrap Start with a short timeout.**
+
+Even with StartTimeout in cfg, a context deadline guards against unexpected hangs.
+
+```diff
+- require.NoError(t, srv.Start(ctx))
++ startCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
++ defer cancel()
++ require.NoError(t, srv.Start(startCtx))
+```
+
+
+π€ Prompt for AI Agents
+
+```
+In examples/temporal-standalone/integration-testing/tests/integration_test.go
+around lines 35 to 44, wrap the call to srv.Start(ctx) in a short
+context.WithTimeout to guard against hangs even if cfg has StartTimeout; create
+a new ctxStart, defer cancel() and call require.NoError(t, srv.Start(ctxStart)),
+keeping the existing cleanup Stop(ctx) unchanged so Stop uses the original test
+context.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez74`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez74
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/012-issue.md b/ai-docs/reviews-pr-305/issues/012-issue.md
new file mode 100644
index 00000000..5b5f03af
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/012-issue.md
@@ -0,0 +1,64 @@
+# Issue 12 - Review Thread Comment
+
+**File:** `go.mod:126`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_π§Ή Nitpick_ | _π΅ Trivial_
+
+**Document accepted AWS S3 Crypto SDK vulns outside go.mod.**
+
+Acceptance note is fine, but prefer moving rationale to SECURITY.md (or a tracking issue) rather than comments in go.mod. Keep the module line clean.
+
+
+π§° Tools
+
+
+πͺ OSV Scanner (2.2.3)
+
+[LOW] 126-126: github.com/aws/aws-sdk-go 1.55.6: In-band key negotiation issue in AWS S3 Crypto SDK for golang in github.com/aws/aws-sdk-go
+
+(GO-2022-0635)
+
+---
+
+[LOW] 126-126: github.com/aws/aws-sdk-go 1.55.6: CBC padding oracle issue in AWS S3 Crypto SDK for golang in github.com/aws/aws-sdk-go
+
+(GO-2022-0646)
+
+
+
+
+
+
+π€ Prompt for AI Agents
+
+```
+In go.mod around lines 124 to 126, the acceptance rationale for AWS S3 Crypto
+SDK vulnerabilities is embedded as comments in the module file; move that
+rationale into SECURITY.md (or a tracked GitHub issue) and reference it from
+go.mod if needed. Remove the comment block from go.mod so the file only contains
+the module dependency line, create or update SECURITY.md with the full
+explanation (including grep verification, vuln IDs GO-2022-0635/GO-2022-0646,
+and justification), and optionally add a one-line comment in go.mod pointing to
+SECURITY.md or the issue number for traceability.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez7-`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez7-
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/013-issue.md b/ai-docs/reviews-pr-305/issues/013-issue.md
new file mode 100644
index 00000000..690b5329
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/013-issue.md
@@ -0,0 +1,64 @@
+# Issue 13 - Review Thread Comment
+
+**File:** `pkg/config/loader.go:447`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_π οΈ Refactor suggestion_ | _π Major_
+
+**Replace remaining 65535 literals with maxTCPPort; unify messages.**
+
+Use the defined maxTCPPort for range checks and format error messages with %d to avoid magic numbers and keep a single source of truth.
+
+Apply:
+
+```diff
+- if standalone.FrontendPort < 1 || standalone.FrontendPort > maxTCPPort {
+- return fmt.Errorf("temporal.standalone.frontend_port must be between 1 and 65535")
++ if standalone.FrontendPort < 1 || standalone.FrontendPort > maxTCPPort {
++ return fmt.Errorf("temporal.standalone.frontend_port must be between 1 and %d", maxTCPPort)
+ }
+@@
+- if standalone.UIPort < 1 || standalone.UIPort > maxTCPPort {
+- return fmt.Errorf("temporal.standalone.ui_port must be between 1 and 65535 when enable_ui is true")
++ if standalone.UIPort < 1 || standalone.UIPort > maxTCPPort {
++ return fmt.Errorf("temporal.standalone.ui_port must be between 1 and %d when enable_ui is true", maxTCPPort)
+ }
+@@
+- } else if standalone.UIPort != 0 && (standalone.UIPort < 1 || standalone.UIPort > 65535) {
+- return fmt.Errorf("temporal.standalone.ui_port must be between 1 and 65535 when set")
++ } else if standalone.UIPort != 0 && (standalone.UIPort < 1 || standalone.UIPort > maxTCPPort) {
++ return fmt.Errorf("temporal.standalone.ui_port must be between 1 and %d when set", maxTCPPort)
+ }
+```
+
+And in validateTCPPort:
+
+```diff
+- if port < 1 || port > 65535 {
+- return fmt.Errorf("%s must be between 1 and 65535, got: %d", fieldName, port)
++ if port < 1 || port > maxTCPPort {
++ return fmt.Errorf("%s must be between 1 and %d, got: %d", fieldName, maxTCPPort, port)
+ }
+```
+As per coding guidelines.
+
+
+Also applies to: 594-604
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8F`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8F
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/014-issue.md b/ai-docs/reviews-pr-305/issues/014-issue.md
new file mode 100644
index 00000000..74ef1d99
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/014-issue.md
@@ -0,0 +1,52 @@
+# Issue 14 - Review Thread Comment
+
+**File:** `test/integration/temporal/mode_switching_test.go:45`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_β οΈ Potential issue_ | _π Major_
+
+**Ensure cleanup of the standalone server.**
+
+Register t.Cleanup right after start to avoid leaks on failures.
+
+Apply:
+
+```diff
+- server := startStandaloneServer(ctx, t, embeddedCfg)
++ server := startStandaloneServer(ctx, t, embeddedCfg)
++ t.Cleanup(func() {
++ stopTemporalServer(ctx, t, server)
++ })
+```
+
+
+π€ Prompt for AI Agents
+
+```
+In test/integration/temporal/mode_switching_test.go around lines 39 to 45, you
+start a standalone server but donβt register cleanup immediately; add t.Cleanup
+right after server := startStandaloneServer(...) to ensure the server is stopped
+on test exit (e.g. t.Cleanup(func() { server.Stop() }) or server.Close()
+depending on the server API), so the server is always torn down even if the test
+fails before later cleanup.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8L`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8L
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/015-issue.md b/ai-docs/reviews-pr-305/issues/015-issue.md
new file mode 100644
index 00000000..4305c4aa
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/015-issue.md
@@ -0,0 +1,51 @@
+# Issue 15 - Review Thread Comment
+
+**File:** `test/integration/temporal/persistence_test.go:50`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_β οΈ Potential issue_ | _π Major_
+
+**Add cleanup for restarted server to prevent leaks.**
+
+The restarted server isnβt stopped; add a cleanup to avoid dangling listeners/locks during CI.
+
+
+```diff
+- restarted := startStandaloneServer(restartCtx, t, restartCfg)
++ restarted := startStandaloneServer(restartCtx, t, restartCfg)
++ t.Cleanup(func() {
++ stopTemporalServer(restartCtx, t, restarted)
++ })
+```
+
+
+π€ Prompt for AI Agents
+
+```
+In test/integration/temporal/persistence_test.go around lines 39 to 50, the
+restarted server started with startStandaloneServer is not being stopped; add a
+cleanup to avoid leaking resources by registering t.Cleanup(func() {
+restarted.Stop() }) immediately after creating restarted (or call the
+appropriate shutdown method if the server type uses a different name, e.g.,
+Close or Shutdown) so the server is stopped when the test finishes.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8N`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8N
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/016-issue.md b/ai-docs/reviews-pr-305/issues/016-issue.md
new file mode 100644
index 00000000..2f45cc28
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/016-issue.md
@@ -0,0 +1,30 @@
+# Issue 16 - Review Thread Comment
+
+**File:** `test/integration/temporal/standalone_test.go:119`
+**Date:** 2025-10-27 13:58:52 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_β οΈ Potential issue_ | _π‘ Minor_
+
+**Consider subtest pattern per project guidelines.**
+
+Project tests should use t.Run("Should ...", ...) subtests. These top-level tests can keep their structure but wrap main assertions in subtests for consistency.
+
+As per coding guidelines.
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8S`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8S
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/017-issue.md b/ai-docs/reviews-pr-305/issues/017-issue.md
new file mode 100644
index 00000000..c298440d
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/017-issue.md
@@ -0,0 +1,45 @@
+# Issue 17 - Review Thread Comment
+
+**File:** `test/integration/temporal/standalone_test.go:207`
+**Date:** 2025-10-27 13:58:53 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_π§Ή Nitpick_ | _π΅ Trivial_
+
+**Unused helper function.**
+
+describeWorkflow is not used. Remove it or use it in TestStandaloneWorkflowExecution to reduce dead code.
+
+
+π€ Prompt for AI Agents
+
+```
+In test/integration/temporal/standalone_test.go around lines 193 to 207, the
+helper function describeWorkflow is unused; either remove it or invoke it from
+TestStandaloneWorkflowExecution. To fix, search for the
+TestStandaloneWorkflowExecution function and, if a workflow description is
+needed there, replace the current direct DescribeWorkflowExecution call (or add
+a call) to use describeWorkflow(ctx, t, address, namespace, workflowID, runID)
+and remove any duplicate client dial/close logic to avoid resource leaks;
+otherwise delete the describeWorkflow function and its tests imports if no
+longer referenced.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8U`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8U
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/018-issue.md b/ai-docs/reviews-pr-305/issues/018-issue.md
new file mode 100644
index 00000000..dccc07bb
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/018-issue.md
@@ -0,0 +1,48 @@
+# Issue 18 - Review Thread Comment
+
+**File:** `test/integration/temporal/startup_lifecycle_test.go:54`
+**Date:** 2025-10-27 13:58:53 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Body
+
+_β οΈ Potential issue_ | _π‘ Minor_
+
+**Adopt subtest pattern for consistency.**
+
+Wrap each scenario body in t.Run("Should ...", ...) to conform to the test style guide.
+
+As per coding guidelines.
+
+
+Also applies to: 56-90, 92-160, 161-179
+
+
+π€ Prompt for AI Agents
+
+```
+In test/integration/temporal/startup_lifecycle_test.go around lines 24-54 (and
+similarly update blocks at 56-90, 92-160, 161-179), the test cases are written
+as top-level code blocks rather than subtests; wrap each scenario body in t.Run
+with a descriptive name like t.Run("Should ", func(t *testing.T) { ...
+}) so they follow the project subtest pattern, move the existing test logic into
+the anonymous func, and ensure any deferred cleanup or t-specific helpers remain
+inside the subtest scope.
+```
+
+
+
+
+
+
+
+## Resolve
+
+Thread ID: `PRRT_kwDOOlCPts5fez8b`
+
+```bash
+gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_kwDOOlCPts5fez8b
+```
+
+---
+*Generated from PR review - CodeRabbit AI*
diff --git a/ai-docs/reviews-pr-305/issues/019-issue.md b/ai-docs/reviews-pr-305/issues/019-issue.md
new file mode 100644
index 00000000..eacaa920
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/019-issue.md
@@ -0,0 +1,31 @@
+# Duplicate from Comment 3
+
+**File:** `scripts/markdown/check.go`
+**Date:** 2025-10-27 13:58:54 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Details
+
+
+scripts/markdown/check.go (1)
+
+`1868-1877`: **Use the named constant instead of literal value.**
+
+Line 1874 uses the literal `5 * time.Second` instead of the `processTerminationGracePeriod` constant defined at the top of the file. For consistency and maintainability, use the constant.
+
+
+Apply this diff:
+
+```diff
+ select {
+ case <-cmdDone:
+ if !useUI {
+ fmt.Fprintf(os.Stderr, "Job %d terminated gracefully after timeout\n", index+1)
+ }
+- case <-time.After(5 * time.Second):
++ case <-time.After(processTerminationGracePeriod):
+ forceKillProcess(cmd, index, useUI)
+ }
+```
+
+
diff --git a/ai-docs/reviews-pr-305/issues/020-issue.md b/ai-docs/reviews-pr-305/issues/020-issue.md
new file mode 100644
index 00000000..c62f4e4b
--- /dev/null
+++ b/ai-docs/reviews-pr-305/issues/020-issue.md
@@ -0,0 +1,16 @@
+# Duplicate from Comment 3
+
+**File:** `test/integration/temporal/persistence_test.go`
+**Date:** 2025-10-27 13:58:54 America/Sao_Paulo
+**Status:** - [x] RESOLVED
+
+## Details
+
+
+test/integration/temporal/persistence_test.go (1)
+
+`38-39`: **Namespace pin on restart addressed.**
+
+Explicitly setting restartCfg.Namespace ensures describe targets the correct namespace across restarts. Good followβthrough on the earlier suggestion.
+
+
diff --git a/cli/help/global-flags.md b/cli/help/global-flags.md
index 9cc05b60..24060c4d 100644
--- a/cli/help/global-flags.md
+++ b/cli/help/global-flags.md
@@ -73,6 +73,45 @@ Forces interactive mode even when CI or non-TTY environment is detected.
- **Config**: `cli.interactive: true`
- **Example**: `compozy auth login --interactive`
+## Temporal Configuration Flags
+
+### `--temporal-mode`
+
+Selects how Compozy connects to Temporal.
+
+- **Values**: `remote`, `standalone`
+- **Default**: `remote`
+- **Environment**: `TEMPORAL_MODE`
+- **Config**: `temporal.mode`
+- **Example**: `compozy start --temporal-mode=standalone`
+
+### `--temporal-standalone-database`
+
+Sets the SQLite database location used by the embedded Temporal server when `--temporal-mode=standalone`.
+
+- **Default**: `:memory:` (ephemeral)
+- **Environment**: `TEMPORAL_STANDALONE_DATABASE_FILE`
+- **Config**: `temporal.standalone.database_file`
+- **Example**: `compozy start --temporal-mode=standalone --temporal-standalone-database=./temporal.db`
+
+### `--temporal-standalone-frontend-port`
+
+Overrides the Temporal frontend gRPC port exposed in standalone mode.
+
+- **Default**: `7233`
+- **Environment**: `TEMPORAL_STANDALONE_FRONTEND_PORT`
+- **Config**: `temporal.standalone.frontend_port`
+- **Example**: `compozy start --temporal-mode=standalone --temporal-standalone-frontend-port=9733`
+
+### `--temporal-standalone-ui-port`
+
+Overrides the Temporal Web UI HTTP port when running in standalone mode.
+
+- **Default**: `8233`
+- **Environment**: `TEMPORAL_STANDALONE_UI_PORT`
+- **Config**: `temporal.standalone.ui_port`
+- **Example**: `compozy start --temporal-mode=standalone --temporal-standalone-ui-port=9833`
+
## Flag Precedence
Configuration values are resolved in the following order (highest to lowest priority):
diff --git a/cli/helpers/flag_categories.go b/cli/helpers/flag_categories.go
index be92b53e..e3e7981e 100644
--- a/cli/helpers/flag_categories.go
+++ b/cli/helpers/flag_categories.go
@@ -173,7 +173,9 @@ func getInfrastructureCategories() []FlagCategory {
Name: "Temporal Configuration",
Description: "Workflow orchestration settings",
Flags: []string{
- "temporal-host", "temporal-namespace", "temporal-task-queue",
+ "temporal-mode", "temporal-host", "temporal-namespace", "temporal-task-queue",
+ "temporal-standalone-database", "temporal-standalone-frontend-port",
+ "temporal-standalone-ui-port",
},
},
{
diff --git a/cli/root.go b/cli/root.go
index 010ac61e..da76acd9 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
+ "path/filepath"
agentscmd "github.com/compozy/compozy/cli/cmd/agents"
authcmd "github.com/compozy/compozy/cli/cmd/auth"
@@ -152,10 +153,10 @@ func buildConfigSources(cmd *cobra.Command, cliFlags map[string]any) []config.So
config.NewDefaultProvider(),
config.NewEnvProvider(),
}
- if configFile := resolveConfigFile(cmd); configFile != "" {
+ cwd := extractCWDFromFlags(cliFlags)
+ configFile := resolveConfigFile(cmd, cwd)
+ if configFile != "" {
sources = append(sources, config.NewYAMLProvider(configFile))
- } else if _, err := os.Stat("compozy.yaml"); err == nil {
- sources = append(sources, config.NewYAMLProvider("compozy.yaml"))
}
if len(cliFlags) > 0 {
sources = append(sources, config.NewCLIProvider(cliFlags))
@@ -163,17 +164,55 @@ func buildConfigSources(cmd *cobra.Command, cliFlags map[string]any) []config.So
return sources
}
-func resolveConfigFile(cmd *cobra.Command) string {
+func resolveConfigFile(cmd *cobra.Command, cwd string) string {
+ configFile := ""
if flag := cmd.PersistentFlags().Lookup("config"); flag != nil {
if value, err := cmd.PersistentFlags().GetString("config"); err == nil {
- return value
+ configFile = value
}
}
- if flag := cmd.Flags().Lookup("config"); flag != nil {
- if value, err := cmd.Flags().GetString("config"); err == nil {
- return value
+ if configFile == "" {
+ if flag := cmd.Flags().Lookup("config"); flag != nil {
+ if value, err := cmd.Flags().GetString("config"); err == nil {
+ configFile = value
+ }
}
}
+ if configFile == "" {
+ configFile = "compozy.yaml"
+ }
+ return resolveConfigPathWithCWD(configFile, cwd)
+}
+
+func extractCWDFromFlags(cliFlags map[string]any) string {
+ if cliFlags == nil {
+ return ""
+ }
+ if cwd, ok := cliFlags["cwd"].(string); ok {
+ return cwd
+ }
+ return ""
+}
+
+func resolveConfigPathWithCWD(configFile, cwd string) string {
+ if configFile == "" {
+ return ""
+ }
+ if filepath.IsAbs(configFile) {
+ if _, err := os.Stat(configFile); err == nil {
+ return configFile
+ }
+ return ""
+ }
+ if cwd != "" {
+ cwdPath := filepath.Join(cwd, configFile)
+ if _, err := os.Stat(cwdPath); err == nil {
+ return cwdPath
+ }
+ }
+ if _, err := os.Stat(configFile); err == nil {
+ return configFile
+ }
return ""
}
diff --git a/docs/content/docs/architecture/embedded-temporal.mdx b/docs/content/docs/architecture/embedded-temporal.mdx
new file mode 100644
index 00000000..f03d3e7a
--- /dev/null
+++ b/docs/content/docs/architecture/embedded-temporal.mdx
@@ -0,0 +1,176 @@
+---
+title: "Embedded Temporal"
+description: "Deep dive into the embedded Temporal server that powers standalone mode."
+icon: Layers
+---
+
+
+Standalone mode embeds the official Temporal server inside the Compozy process. It spins up the same four microservices you deploy in productionβjust scoped to the developer machine.
+
+
+## Component Topology
+
+|gRPC| HI
+ FE -->|Task routing| MA
+ MA --> WO
+ HI --> DB[(SQLite)]
+ WO --> DB
+ FE --> DB
+`} />
+
+
+
+
+ Component
+ Role
+ Default Port
+
+
+
+
+ Frontend
+ gRPC entrypoint used by Compozy workers and the CLI
+ 7233
+
+
+ History
+ Persists workflow execution history and timers
+ 7234
+
+
+ Matching
+ Distributes tasks to the correct worker queues
+ 7235
+
+
+ Worker
+ Runs system workflows (namespace replication, visibility)
+ 7236
+
+
+ UI Server
+ Optional Temporal Web UI for debugging workflows
+ 8233
+
+
+
+
+## Port Allocation Strategy
+
+- `frontend_port` anchors the service block; History, Matching, and Worker consume the next three sequential ports.
+- Ports bind to `standalone.bind_ip` (defaults to `127.0.0.1`). Override the IP when running inside containers that expose additional interfaces.
+- Adjust `frontend_port` to avoid conflicts, for example when other Temporal stacks are already listening on 7233.
+
+```yaml title="Custom port block"
+temporal:
+ mode: standalone
+ standalone:
+ frontend_port: 9733 # Services listen on 9733-9736
+ bind_ip: 0.0.0.0 # Use cautiously; exposes gRPC outside loopback
+ enable_ui: true
+ ui_port: 9820
+```
+
+
+Binding to `0.0.0.0` opens the embedded Temporal services beyond the local machine. Only do this inside disposable containers or secured developer networks.
+
+
+## SQLite Persistence Modes
+
+
+
+ `database_file: :memory:` keeps Temporal state in RAM. This is fastest for unit tests because every Compozy restart resets state.
+
+
+ Point `database_file` to a writable path to persist workflow history between restarts. The server enables WAL + NORMAL sync for durability without sacrificing dev speed.
+
+
+
+The builder automatically selects SQLite connection attributes:
+
+```go title="engine/worker/embedded/sqlite.go"
+if dbFile == ":memory:" {
+ return map[string]string{"mode": "memory", "cache": "shared"}
+}
+return map[string]string{"_journal_mode": "WAL", "_synchronous": "NORMAL"}
+```
+
+
+Use file-backed SQLite in CI if you need to inspect workflow history after a failure. Clean up the file between test runs to avoid cross-test contamination.
+
+
+## Lifecycle Management
+
+
+
+ `maybeStartStandaloneTemporal` evaluates `temporal.mode`. When set to `standalone`, it constructs Temporal configuration, assigns deterministic host settings, and launches the server using `server.NewServer()`.
+
+
+ Startup waits for the frontend service to accept gRPC connections. The wait is bounded by `standalone.start_timeout` (default `30s`).
+
+
+ On first boot, the embedded server creates the configured namespace (`standalone.namespace`) and cluster name (`standalone.cluster_name`).
+
+
+ Compozy stops the worker pool and gracefully closes the Temporal server, flushing SQLite WAL files to disk when applicable.
+
+
+
+## Logging & Observability
+
+- `standalone.log_level` controls Temporal server logging (`debug`, `info`, `warn`, `error`). Logs flow through `logger.FromContext(ctx)`.
+- The embedded UI exposes workflow executions, task queues, and history events via `http://localhost:8233`.
+- Prometheus metrics are emitted on the same process; scrape the Compozy metrics endpoint to monitor Temporal internals during development.
+
+## Security Considerations
+
+
+Standalone mode intentionally trades durability and network isolation for convenience. Do not share the embedded SQLite database between users, and never expose the Temporal UI without authentication.
+
+
+- Keep `bind_ip` on loopback unless you understand the risk profile.
+- Use non-default namespaces and task queues when multiple developers share a Temporal cluster.
+- Reset the SQLite file after workshops to avoid leaking PII in workflow payloads.
+
+## Related Guides
+
+
+
+
+
+
+
diff --git a/docs/content/docs/architecture/meta.json b/docs/content/docs/architecture/meta.json
new file mode 100644
index 00000000..6eac6269
--- /dev/null
+++ b/docs/content/docs/architecture/meta.json
@@ -0,0 +1,9 @@
+{
+ "title": "Architecture",
+ "description": "Deep dives into Compozy's architecture and deployment internals",
+ "icon": "Layers",
+ "root": true,
+ "pages": [
+ "embedded-temporal"
+ ]
+}
diff --git a/docs/content/docs/cli/compozy-start.mdx b/docs/content/docs/cli/compozy-start.mdx
new file mode 100644
index 00000000..e2f3ce2d
--- /dev/null
+++ b/docs/content/docs/cli/compozy-start.mdx
@@ -0,0 +1,144 @@
+---
+title: "compozy start"
+description: "Production-grade start command with Temporal configuration flags."
+icon: Terminal
+---
+
+The `compozy start` command boots the Compozy production server. With Temporal standalone mode it can also launch the embedded Temporal server, making end-to-end execution possible without external infrastructure.
+
+
+Use `--temporal-mode=standalone` for development and CI only. Keep production deployments pinned to `--temporal-mode=remote` and an external Temporal cluster.
+
+
+## Usage
+
+```bash
+compozy start [flags]
+```
+
+### Temporal Flags
+
+
+
+
+ Flag
+ Description
+ Default
+
+
+
+
+ `--temporal-mode`
+ Selects Temporal connectivity (`remote` or `standalone`).
+ `remote`
+
+
+ `--temporal-host`
+ Overrides the Temporal gRPC endpoint when running in remote mode.
+ `localhost:7233`
+
+
+ `--temporal-namespace`
+ Specifies the Temporal namespace to target.
+ `default`
+
+
+ `--temporal-task-queue`
+ Sets the worker task queue name.
+ `compozy-tasks`
+
+
+ `--temporal-standalone-database`
+ SQLite path for embedded Temporal (`:memory:` or file path).
+ `:memory:`
+
+
+ `--temporal-standalone-frontend-port`
+ Anchors the service port block (services listen on `port`-`port+3`).
+ `7233`
+
+
+ `--temporal-standalone-ui-port`
+ HTTP port for the Temporal Web UI when enabled.
+ `8233`
+
+
+
+
+Additional standalone fields (`bind_ip`, `namespace`, `cluster_name`, `enable_ui`, `log_level`, `start_timeout`) are configured via `compozy.yaml` or environment variables.
+
+## Examples
+
+
+
+
+ ```bash title="Production"
+ compozy start \
+ --temporal-mode=remote \
+ --temporal-host=temporal.internal:7233 \
+ --temporal-namespace=compozy-prod
+ ```
+
+
+
+
+ ```bash title="Local development"
+ compozy start \
+ --temporal-mode=standalone \
+ --temporal-standalone-database=:memory: \
+ --temporal-standalone-frontend-port=9733 \
+ --temporal-standalone-ui-port=9833
+ ```
+
+
+
+
+
+During development you can keep `temporal.mode=standalone` in `compozy.yaml` and omit the flag entirely; the CLI flag still wins if you need to override temporarily.
+
+
+## Flag Precedence
+
+
+
+ Highest priority. Use flags for ad-hoc overrides or CI pipelines.
+
+
+ Values defined in `compozy.yaml` apply when flags are not supplied.
+
+
+ `TEMPORAL_MODE`, `TEMPORAL_HOST_PORT`, and friends populate defaults when neither flags nor config values are provided.
+
+
+ Registry defaults supply sensible values for local development (remote mode + localhost ports).
+
+
+
+## Operational Notes
+
+- Port availability checks ensure standalone mode fails fast if 7233-7236 or 8233 are busy.
+- `start_timeout` controls how long the CLI waits for the embedded server to go healthy.
+- Logs flow through `logger.FromContext(ctx)`; increase verbosity with `--log-level debug` alongside `--temporal-standalone-log-level=debug` in configuration.
+
+## Related Content
+
+
+
+
+
+
diff --git a/docs/content/docs/cli/meta.json b/docs/content/docs/cli/meta.json
index a4deb494..582eb049 100644
--- a/docs/content/docs/cli/meta.json
+++ b/docs/content/docs/cli/meta.json
@@ -6,6 +6,7 @@
"pages": [
"project-commands",
"dev-commands",
+ "compozy-start",
"auth-commands",
"config-commands",
"knowledge-commands",
diff --git a/docs/content/docs/configuration/meta.json b/docs/content/docs/configuration/meta.json
new file mode 100644
index 00000000..48c4971d
--- /dev/null
+++ b/docs/content/docs/configuration/meta.json
@@ -0,0 +1,9 @@
+{
+ "title": "Configuration",
+ "description": "Configuration references and examples",
+ "icon": "Settings",
+ "root": true,
+ "pages": [
+ "temporal"
+ ]
+}
diff --git a/docs/content/docs/configuration/temporal.mdx b/docs/content/docs/configuration/temporal.mdx
new file mode 100644
index 00000000..e23b8ed8
--- /dev/null
+++ b/docs/content/docs/configuration/temporal.mdx
@@ -0,0 +1,277 @@
+---
+title: "Temporal Configuration"
+description: "Reference for configuring Temporal connectivity in Compozy."
+icon: Settings
+---
+
+
+Temporal configuration is split into **mode selection** and **standalone overrides**. Remote mode connects to an external cluster; standalone mode boots `temporal.NewServer()` inside the process for local development and CI.
+
+
+## Configuration Structure
+
+```yaml title="compozy.yaml"
+temporal:
+ mode: remote | standalone
+ host_port: localhost:7233
+ namespace: default
+ task_queue: compozy-tasks
+ standalone:
+ database_file: :memory:
+ frontend_port: 7233
+ bind_ip: 127.0.0.1
+ namespace: default
+ cluster_name: compozy-standalone
+ enable_ui: true
+ ui_port: 8233
+ log_level: warn
+ start_timeout: 30s
+```
+
+## Mode Selection
+
+
+
+
+ Field
+ Description
+ Default
+
+
+
+
+ `mode`
+ `remote` (default) uses an external Temporal cluster. `standalone` starts the embedded Temporal server.
+ `remote`
+
+
+ `host_port`
+ Temporal endpoint in `host:port` format. Overridden automatically when `mode=standalone`.
+ `localhost:7233`
+
+
+ `namespace`
+ Default Temporal namespace for workflows. Create unique namespaces per environment.
+ `default`
+
+
+ `task_queue`
+ Primary task queue that Compozy workers poll.
+ `compozy-tasks`
+
+
+
+
+
+When `mode=standalone`, `host_port` is rewritten to `bind_ip:frontend_port`. Update downstream services (like external workers) to use the new address if you expose it beyond localhost.
+
+
+## Standalone Options
+
+
+
+
+ Field
+ Description
+ Default
+
+
+
+
+ `database_file`
+ SQLite path. Use `:memory:` for ephemeral instances or a file path for persistence.
+ `:memory:`
+
+
+ `frontend_port`
+ Anchors the port block used by Temporal services (frontend + history + matching + worker).
+ `7233`
+
+
+ `bind_ip`
+ Interface to bind Temporal services. Keep `127.0.0.1` for safe development defaults.
+ `127.0.0.1`
+
+
+ `namespace`
+ Namespace created automatically when the embedded server starts.
+ `default`
+
+
+ `cluster_name`
+ Cluster identifier for the embedded server. Useful when inspecting metrics or logs.
+ `compozy-standalone`
+
+
+ `enable_ui`
+ Toggle the Temporal Web UI bundled with standalone mode.
+ `true`
+
+
+ `ui_port`
+ HTTP port for the Temporal Web UI.
+ `8233`
+
+
+ `log_level`
+ Temporal server log level (`debug`, `info`, `warn`, `error`).
+ `warn`
+
+
+ `start_timeout`
+ Maximum time to wait for Temporal startup before failing (`time.Duration` format).
+ `30s`
+
+
+
+
+## Environment Variables & CLI Flags
+
+
+
+
+ Config Path
+ Environment Variable
+ CLI Flag
+
+
+
+
+ `temporal.mode`
+ `TEMPORAL_MODE`
+ `--temporal-mode`
+
+
+ `temporal.host_port`
+ `TEMPORAL_HOST_PORT`
+ `--temporal-host`
+
+
+ `temporal.namespace`
+ `TEMPORAL_NAMESPACE`
+ `--temporal-namespace`
+
+
+ `temporal.task_queue`
+ `TEMPORAL_TASK_QUEUE`
+ `--temporal-task-queue`
+
+
+ `temporal.standalone.database_file`
+ `TEMPORAL_STANDALONE_DATABASE_FILE`
+ `--temporal-standalone-database`
+
+
+ `temporal.standalone.frontend_port`
+ `TEMPORAL_STANDALONE_FRONTEND_PORT`
+ `--temporal-standalone-frontend-port`
+
+
+ `temporal.standalone.bind_ip`
+ `TEMPORAL_STANDALONE_BIND_IP`
+ -
+
+
+ `temporal.standalone.namespace`
+ `TEMPORAL_STANDALONE_NAMESPACE`
+ -
+
+
+ `temporal.standalone.cluster_name`
+ `TEMPORAL_STANDALONE_CLUSTER_NAME`
+ -
+
+
+ `temporal.standalone.enable_ui`
+ `TEMPORAL_STANDALONE_ENABLE_UI`
+ -
+
+
+ `temporal.standalone.ui_port`
+ `TEMPORAL_STANDALONE_UI_PORT`
+ `--temporal-standalone-ui-port`
+
+
+ `temporal.standalone.log_level`
+ `TEMPORAL_STANDALONE_LOG_LEVEL`
+ -
+
+
+ `temporal.standalone.start_timeout`
+ `TEMPORAL_STANDALONE_START_TIMEOUT`
+ -
+
+
+
+
+## Validation Rules
+
+
+
+ Only `remote` and `standalone` are accepted values for `temporal.mode`.
+
+
+ `frontend_port` and `ui_port` must be between 1 and 65535. `frontend_port` reserves a block of four contiguous ports.
+
+
+ `bind_ip` must be a valid IP string. IPv4 loopback is recommended for development.
+
+
+ `log_level` accepts `debug`, `info`, `warn`, or `error`.
+
+
+ `start_timeout` must be positive. Increase it if your machine boots Temporal slower than 30 seconds.
+
+
+
+## Usage Patterns
+
+
+
+
+ ```bash title="Remote mode deployment"
+ export TEMPORAL_MODE=remote
+ export TEMPORAL_HOST_PORT=temporal.prod.internal:7233
+ compozy start --temporal-namespace=compozy-prod
+ ```
+
+
+
+
+ ```bash title="Standalone mode development"
+ export TEMPORAL_MODE=standalone
+ export TEMPORAL_STANDALONE_DATABASE_FILE=./.tmp/temporal.db
+ compozy start --temporal-standalone-frontend-port=9733 --temporal-standalone-ui-port=9833
+ ```
+
+
+
+
+## Related Documentation
+
+
+
+
+
+
+
diff --git a/docs/content/docs/core/deployment/meta.json b/docs/content/docs/core/deployment/meta.json
deleted file mode 100644
index 687cd979..00000000
--- a/docs/content/docs/core/deployment/meta.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "title": "Deployment",
- "description": "Deployment options and strategies",
- "icon": "Rocket",
- "pages": [
- "docker",
- "kubernetes"
- ]
-}
diff --git a/docs/content/docs/core/tasks/collection-tasks.mdx b/docs/content/docs/core/tasks/collection-tasks.mdx
index fce6b711..6336e63c 100644
--- a/docs/content/docs/core/tasks/collection-tasks.mdx
+++ b/docs/content/docs/core/tasks/collection-tasks.mdx
@@ -654,7 +654,7 @@ For production deployments, consider implementing [monitoring and observability]
diff --git a/docs/content/docs/core/deployment/docker.mdx b/docs/content/docs/deployment/docker.mdx
similarity index 99%
rename from docs/content/docs/core/deployment/docker.mdx
rename to docs/content/docs/deployment/docker.mdx
index f2e5e375..eed9fef3 100644
--- a/docs/content/docs/core/deployment/docker.mdx
+++ b/docs/content/docs/deployment/docker.mdx
@@ -379,7 +379,7 @@ docker logs compozy-server --tail 50
+Standalone mode is optimized for development and testing only. Production deployments **must** use the remote mode backed by a highly available Temporal cluster.
+
+
+## Deployment Checklist
+
+
+
+ Use Temporal Cloud or a self-managed Temporal cluster with multi-node frontend, history, matching, and worker services. Verify TLS and authentication.
+
+
+ Provision managed PostgreSQL for Compozy metadata and Redis for caching, rate limiting, and configuration layers.
+
+
+ Load secrets from your platform (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) and mount `compozy.yaml` with read-only permissions.
+
+
+ Enable structured logging, metrics export, and distributed tracing in both Compozy and Temporal. Ensure alerts cover queue backlogs and workflow failures.
+
+
+
+## Recommended Configuration
+
+Configure remote mode explicitly in your project configuration. Compozy will refuse to start if `temporal.mode` is missing in production builds.
+
+```yaml title="compozy.yaml"
+server:
+ environment: production
+
+temporal:
+ mode: remote
+ host_port: temporal.my-company.internal:7233
+ namespace: compozy-prod
+ task_queue: compozy-tasks
+
+runtime:
+ environment: production
+```
+
+```bash title="Start with hardened settings"
+COMPOZY_CONFIG_FILE=./compozy.yaml \
+ compozy start \
+ --format json \
+ --temporal-mode=remote \
+ --temporal-host=temporal.my-company.internal:7233
+```
+
+
+Validate the remote Temporal cluster before switching traffic. Run `compozy diagnostics temporal` (if enabled) or `temporal workflow list` to confirm connectivity and namespace health.
+
+
+## Best Practices
+
+
+
+
+ Concern
+ Guidance
+
+
+
+
+ High availability
+ Deploy Temporal with at least 3 history and 2 frontend instances. Use load balancers with health checks on port 7233.
+
+
+ Security
+ Enable mTLS between Compozy and Temporal. Lock down the Temporal Web UI to trusted networks only.
+
+
+ Namespaces
+ Use dedicated namespaces per environment (dev, staging, prod) to isolate retention policies and rate limits.
+
+
+ Task queues
+ Partition workloads using multiple task queues when different latency or concurrency guarantees are required.
+
+
+ Disaster recovery
+ Configure Temporal replication or backups. Test failover playbooks quarterly.
+
+
+
+
+## Migration Tips
+
+- Plan a low-traffic maintenance window.
+- Update `temporal.mode` to `remote` in configuration management.
+- Point workers to the production Temporal cluster.
+- Restart Compozy servers to pick up the new configuration.
+- Monitor workflow metrics and queues closely for the first 30 minutes.
+
+## See Also
+
+
+
+
+
+
diff --git a/docs/content/docs/deployment/temporal-modes.mdx b/docs/content/docs/deployment/temporal-modes.mdx
new file mode 100644
index 00000000..44354d64
--- /dev/null
+++ b/docs/content/docs/deployment/temporal-modes.mdx
@@ -0,0 +1,223 @@
+---
+title: "Temporal Modes"
+description: "Choose between remote and standalone Temporal deployments for Compozy."
+icon: GitBranch
+---
+
+
+**Production rule:** Use the remote mode for every production environment. Standalone mode embeds Temporal for local development and automated testing only.
+
+
+## Mode Comparison
+
+
+
+
+ Capability
+ Remote Mode
+ Standalone Mode
+
+
+
+
+ Target environment
+ Production, staging, shared QA
+ Local development, ephemeral CI jobs, automated tests
+
+
+ Runtime
+ External Temporal cluster (Temporal Cloud or self-managed)
+ Embedded `temporal.NewServer()` running in-process
+
+
+ Persistence
+ Managed persistence (Cassandra, MySQL, PostgreSQL)
+ SQLite (`:memory:` or file on disk)
+
+
+ High availability
+ Multi-node, supports failover and replication
+ Single-node; no HA guarantees
+
+
+ Web UI
+ Optional, usually deployed separately
+ Bundled UI on port 8233 when `enable_ui: true`
+
+
+ Default ports
+ Depends on cluster setup (usually 7233)
+ Frontend 7233, History 7234, Matching 7235, Worker 7236, UI 8233
+
+
+ Startup latency
+ Depends on remote connectivity
+ < 10s typical; configurable via `start_timeout`
+
+
+
+
+## Architecture Overview
+
+ H
+ F -->|GRPC 7233| Compozy
+ H --> DB
+ M --> DB
+ W --> DB
+ end
+
+ subgraph Compozy standalone
+ subgraph Embedded Temporal
+ FE[Frontend 7233]
+ HI[History 7234]
+ MA[Matching 7235]
+ WO[Worker 7236]
+ DB2[(SQLite :memory: or file)]
+ end
+ UI[Temporal UI 8233]
+ end
+
+ Compozy -.-> UI
+ FE --> DB2
+ HI --> DB2
+ MA --> DB2
+ WO --> DB2
+`} />
+
+
+The embedded server reuses the production Temporal codebase. Switching between modes changes orchestration, not workflow semantics.
+
+
+## Remote Mode (Production)
+
+Use remote mode whenever you need durability, multi-node availability, or shared infrastructure.
+
+```yaml title="temporal (remote)"
+temporal:
+ mode: remote
+ host_port: temporal.my-company.internal:7233
+ namespace: compozy-prod
+ task_queue: compozy-tasks
+```
+
+```bash title="CLI override"
+compozy start --temporal-mode=remote --temporal-host=temporal.my-company.internal:7233
+```
+
+- Provision namespaces per environment and align retention policies with compliance needs.
+- Enable mTLS or mutual auth on the Temporal gateway.
+- Monitor latency, workflow backlog, and activity heartbeats via Temporal metrics.
+
+## Standalone Mode (Development & CI)
+
+Standalone mode spins up the Temporal server inside the Compozy process. It is perfect for developers who want zero external dependencies.
+
+```yaml title="temporal (standalone)"
+temporal:
+ mode: standalone
+ host_port: localhost:7233
+ standalone:
+ database_file: :memory:
+ frontend_port: 7233
+ bind_ip: 127.0.0.1
+ namespace: default
+ cluster_name: compozy-standalone
+ enable_ui: true
+ ui_port: 8233
+ log_level: warn
+ start_timeout: 30s
+```
+
+```bash title="Quick start"
+compozy start --temporal-mode=standalone --temporal-standalone-database=:memory:
+```
+
+
+Standalone mode stores workflow state in SQLite and binds to loopback by default. Do not expose it to the public internet and never run it behind load balancers.
+
+
+### When to Use Standalone
+
+
+
+ Reduce onboarding frictionβno Docker Compose, just run `compozy start` and start building workflows.
+
+
+ Run integration tests inside CI jobs without provisioning external clusters. Point tests to `TEMPORAL_MODE=standalone` for predictable behavior.
+
+
+ Ship self-contained examples that boot end-to-end in seconds, ideal for tutorials and demos.
+
+
+
+### Port Allocation
+
+| Service | Port | Description |
+| --------- | ---- | ----------- |
+| Frontend | 7233 | gRPC entrypoint for clients and workers |
+| History | 7234 | Internal history service |
+| Matching | 7235 | Task queue matching service |
+| Worker | 7236 | System worker service (internal workflows) |
+| Web UI | 8233 | Temporal Web UI (when `enable_ui` is `true`) |
+
+Use the `frontend_port` field to shift the entire block of service ports. For example, setting `frontend_port: 9733` exposes the services on 9733β9736.
+
+## Migrating Between Modes
+
+
+
+ Audit current workflows, ensure retries are idempotent, and decide migration direction (remote β standalone for dev, or standalone β remote for prod).
+
+
+ Change `temporal.mode` and related fields in `compozy.yaml`. For standalone β remote, remove the `standalone` block to rely on remote defaults.
+
+
+ Adjust environment variables (`TEMPORAL_MODE`, `TEMPORAL_HOST_PORT`, `TEMPORAL_STANDALONE_*`) and restart any long-running workers.
+
+
+ For remote mode, run `temporal namespace describe`. For standalone mode, open `http://localhost:8233` to confirm the embedded UI is available.
+
+
+
+
+For automated tests, pin `database_file` to a temporary location (e.g. `./.tmp/temporal.db`) when you need persistence across multiple test executions.
+
+
+## Resources
+
+
+
+
+
+
+
+
+- [Temporal Self-Hosted Guide](https://docs.temporal.io/self-hosted-guide)
+- [Reference implementation using `temporal.NewServer()`](https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go)
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index 3825c841..476cbad4 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -1,5 +1,10 @@
{
"pages": [
+ "quick-start",
+ "deployment",
+ "configuration",
+ "architecture",
+ "troubleshooting",
"core",
"cli",
"schema",
diff --git a/docs/content/docs/quick-start/index.mdx b/docs/content/docs/quick-start/index.mdx
new file mode 100644
index 00000000..2487b763
--- /dev/null
+++ b/docs/content/docs/quick-start/index.mdx
@@ -0,0 +1,195 @@
+---
+title: "Quick Start"
+description: "Launch Compozy with the embedded Temporal server in under five minutes."
+icon: Rocket
+---
+
+
+This quick start uses **Temporal standalone mode**, so you do not need Docker or an external Temporal cluster. For production hardening, read the [Production guide](/docs/deployment/production).
+
+
+## Prerequisites
+
+- [Compozy CLI](/docs/core/getting-started/installation)
+- [Bun](https://bun.sh/docs/installation) for tool execution
+- An OpenAI-compatible API key stored in `.env`
+
+## 1. Initialize a Project
+
+```bash
+compozy init hello-standalone
+cd hello-standalone
+```
+
+## 2. Configure Temporal Standalone Mode
+
+Replace the generated `compozy.yaml` with the configuration below. It enables standalone mode and points to a simple workflow.
+
+```yaml title="compozy.yaml"
+name: hello-standalone
+version: "0.1.0"
+description: Develop locally with embedded Temporal
+
+temporal:
+ mode: standalone
+ host_port: localhost:7233
+ standalone:
+ database_file: :memory:
+ frontend_port: 7233
+ bind_ip: 127.0.0.1
+ namespace: default
+ cluster_name: compozy-standalone
+ enable_ui: true
+ ui_port: 8233
+
+runtime:
+ type: bun
+ entrypoint: "./entrypoint.ts"
+ permissions:
+ - --allow-read
+ - --allow-net
+
+workflows:
+ - source: ./workflows/greeting.yaml
+```
+
+## 3. Create a Tool and Workflow
+
+```typescript title="entrypoint.ts"
+interface GreetingInput {
+ name: string;
+}
+
+export default {
+ async greeting_tool({ input }: { input: GreetingInput }) {
+ return {
+ message: `Hello, ${input.name}! Welcome to Compozy with Temporal standalone mode.`,
+ timestamp: new Date().toISOString(),
+ };
+ },
+};
+```
+
+```yaml title="workflows/greeting.yaml"
+id: greeting-workflow
+version: 0.1.0
+description: Quick start workflow using embedded Temporal
+
+schemas:
+ - id: greeting_input
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the person to greet
+ required:
+ - name
+
+config:
+ input:
+ greeting_input
+
+tools:
+ - id: greeting_tool
+ description: Generates a greeting message
+ input:
+ greeting_input
+
+agents:
+ - id: greeter
+ model: openai:gpt-4o-mini
+ instructions: "Use the greeting tool to produce a personalized welcome."
+ tools:
+ - greeting_tool
+ actions:
+ - id: make_greeting
+ prompt: "Generate a greeting for {{ .workflow.input.name }}."
+
+tasks:
+ - id: greet
+ type: basic
+ agent: greeter
+ action: make_greeting
+ final: true
+
+outputs:
+ greeting: "{{ .tasks.greet.output }}"
+```
+
+Create a `.env` file with your API key:
+
+```bash
+OPENAI_API_KEY=sk-your-api-key
+```
+
+## 4. Start Compozy with Standalone Temporal
+
+```bash
+compozy start --temporal-mode=standalone --temporal-standalone-database=:memory:
+```
+
+The command boots Compozy, launches the embedded Temporal services on ports 7233-7236, and serves the Temporal Web UI at `http://localhost:8233`.
+
+
+Visit the Temporal Web UI to watch workflow executions in real time. Use it to inspect task queues, workflow history, and failure retries without leaving your laptop.
+
+
+## 5. Run the Workflow
+
+
+
+
+ ```bash
+ compozy workflow run ./workflows/greeting.yaml --input '{"name":"Avery"}'
+ ```
+
+
+
+
+ ```http title="test.http"
+ @baseUrl = http://localhost:5001/api/v0
+ @workflowId = greeting-workflow
+
+ POST {{baseUrl}}/workflows/{{workflowId}}/executions
+ Content-Type: application/json
+
+ {
+ "input": {
+ "name": "Avery"
+ }
+ }
+ ```
+
+
+
+
+You should see the greeting response in your terminal or HTTP client within seconds.
+
+## Next Steps
+
+
+
+
+
+
+
diff --git a/docs/content/docs/quick-start/meta.json b/docs/content/docs/quick-start/meta.json
new file mode 100644
index 00000000..853f0155
--- /dev/null
+++ b/docs/content/docs/quick-start/meta.json
@@ -0,0 +1,9 @@
+{
+ "title": "Quick Start",
+ "description": "Get Compozy running with the embedded Temporal server",
+ "icon": "Rocket",
+ "root": true,
+ "pages": [
+ "index"
+ ]
+}
diff --git a/docs/content/docs/troubleshooting/meta.json b/docs/content/docs/troubleshooting/meta.json
new file mode 100644
index 00000000..833380de
--- /dev/null
+++ b/docs/content/docs/troubleshooting/meta.json
@@ -0,0 +1,9 @@
+{
+ "title": "Troubleshooting",
+ "description": "Resolve common issues when operating Compozy",
+ "icon": "LifeBuoy",
+ "root": true,
+ "pages": [
+ "temporal"
+ ]
+}
diff --git a/docs/content/docs/troubleshooting/temporal.mdx b/docs/content/docs/troubleshooting/temporal.mdx
new file mode 100644
index 00000000..b476c9a7
--- /dev/null
+++ b/docs/content/docs/troubleshooting/temporal.mdx
@@ -0,0 +1,122 @@
+---
+title: "Temporal Troubleshooting"
+description: "Resolve common issues when running Temporal in Compozy."
+icon: LifeBuoy
+---
+
+
+Most issues surface when running standalone mode because the Temporal server shares ports and resources with your local machine. Start here before escalating to Temporal logs.
+
+
+## Quick Diagnostics
+
+
+
+ `lsof -i :7233-7236` shows which process is using Temporal service ports. Release conflicting processes or configure a different `frontend_port`.
+
+
+ Run `COMPOZY_LOG_LEVEL=debug` and set `temporal.standalone.log_level=debug` to stream Temporal server logs through the CLI.
+
+
+ `temporal namespace describe` confirms the namespace exists and is reachable when operating in remote mode.
+
+
+
+## Port Conflicts
+
+
+
+
+ Symptom
+ Resolution
+
+
+
+
+ `listen tcp 127.0.0.1:7233: bind: address already in use`
+ Change `temporal.standalone.frontend_port` (all services shift with it) or stop the conflicting process.
+
+
+ Temporal UI fails to start because 8233 is in use
+ Set `temporal.standalone.ui_port` to an available port or disable the UI with `enable_ui: false`.
+
+
+ Remote mode cannot reach Temporal
+ Verify firewall rules and TLS configuration. Use `temporal workflow list --namespace ` from the same host.
+
+
+
+
+## SQLite Issues
+
+
+
+ Ensure the directory containing `database_file` exists and is writable. For CI, create a tmp directory before launching Compozy.
+
+
+ Delete the SQLite file and restart. Use `:memory:` for transient runs or swap to a clean path per test.
+
+
+ Large disk-backed SQLite files slow boot time. Prune old history or use `:memory:` during development.
+
+
+
+## Startup Timeouts
+
+If you see `standalone Temporal failed to start within 30s`:
+
+- Increase `temporal.standalone.start_timeout` (e.g. `60s`) for slower laptops or CI.
+- Check that the host ports are free; Temporal will not start if they are already bound.
+- For remote mode, confirm the Temporal cluster is reachable and healthy. Latency or TLS negotiation failures can exhaust the timeout.
+
+## Temporal UI Not Accessible
+
+- Confirm `enable_ui: true` and that the UI port is open.
+- For standalone mode, open `http://127.0.0.1:`. If you bound to `0.0.0.0`, use the machine IP explicitly.
+- Behind reverse proxies, configure allowed hosts and TLS termination before exposing the UI to other users.
+
+## Performance & Throughput
+
+- Standalone mode uses SQLite and a single worker processβexpect lower throughput than production clusters.
+- To simulate production load, switch to remote mode against a staging Temporal cluster.
+- Monitor metrics via Compozyβs Prometheus endpoint; look for high `workflow_task_schedule_to_start_latency` when load grows.
+
+## Escalation Checklist
+
+
+
+ Attach Compozy and Temporal logs when filing an issue.
+
+
+ Include `temporal` and `standalone` sections of `compozy.yaml` and any CLI overrides you used.
+
+
+ Provide the Compozy version, Go runtime version, and Temporal server version if running remotely.
+
+
+
+## Related Resources
+
+
+
+
+
+
+
+- [Temporal Server Operational Guide](https://docs.temporal.io/self-hosted-guide)
+- [Temporalite Deprecation Notice](https://github.com/temporalio/temporalite#deprecation-notice)
diff --git a/docs/native-tools.md b/docs/native-tools.md
deleted file mode 100644
index ea8160e0..00000000
--- a/docs/native-tools.md
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: Compozy Native Tools (cp__*)
-description: Built-in, sandboxed tools for reliable local execution
----
-
-Overview
-
-- Compozy ships built-in, sandboxed tools exposed with the cp\_\_ prefix.
-- These tools replace legacy @compozy/tool-\* npm packages.
-- Benefits: zero external runtime, uniform error catalog, consistent telemetry.
-
-Core Tools
-
-- cp**read_file, cp**write_file, cp**delete_file, cp**list_dir, cp\_\_grep
-- cp\_\_exec (allowlisted absolute paths, capped stdout/stderr, timeouts)
-- cp\_\_fetch (HTTP(S) only, size and redirect caps)
-- cp\_\_agent_orchestrate (inline multi-agent planning and execution)
-- cp\_\_list_agents, cp\_\_describe_agent (agent catalog discovery helpers)
-
-Configuration
-
-- Configure via application config: NativeTools.RootDir, NativeTools.Exec.Allowlist
-- Use config.FromContext(ctx) in code paths; never global singletons.
-
-Observability
-
-- Each invocation emits structured logs with tool_id and request_id.
-- Prometheus metrics: compozy_tool_invocations_total, compozy_tool_latency_seconds, compozy_tool_response_bytes.
-- Errors map to canonical codes: InvalidArgument, PermissionDenied, FileNotFound, CommandNotAllowed, Internal.
-
-Migration Guide
-
-- Replace imports of @compozy/tool-\* with cp\_\_ tool IDs in agent/tool configs.
-- Remove Bun workspace dependencies for tools in package.json.
-- Update docs/examples to reference cp\_\_ identifiers exclusively.
-
-Troubleshooting
-
-- PermissionDenied: check sandbox root and allowlist settings.
-- InvalidArgument: verify schema fields (path/url/method, etc.).
-- Timeouts: adjust per-tool timeout_ms within configured caps.
diff --git a/docs/source.config.ts b/docs/source.config.ts
index 60f433a4..a17d57fb 100644
--- a/docs/source.config.ts
+++ b/docs/source.config.ts
@@ -12,7 +12,31 @@ export const docs = defineDocs({
},
});
-export default defineConfig({
+export interface NavigationLink {
+ title: string;
+ url: string;
+ description: string;
+}
+
+const navigationLinks: NavigationLink[] = [
+ {
+ title: "Temporal Modes",
+ url: "/docs/deployment/temporal-modes",
+ description: "Choose between remote and standalone Temporal modes",
+ },
+ {
+ title: "Embedded Temporal",
+ url: "/docs/architecture/embedded-temporal",
+ description: "Technical deep-dive on embedded Temporal server implementation",
+ },
+ {
+ title: "Temporal Troubleshooting",
+ url: "/docs/troubleshooting/temporal",
+ description: "Common Temporal issues and solutions",
+ },
+];
+
+const config = defineConfig({
mdxOptions: {
rehypeCodeOptions: {
themes: {
@@ -36,3 +60,8 @@ export default defineConfig({
},
},
});
+
+// fumadocs-mdx does not expose navigationLinks in the config type; attach via runtime assignment
+(config as { navigationLinks?: typeof navigationLinks }).navigationLinks = navigationLinks;
+
+export default config;
diff --git a/engine/infra/server/dependencies.go b/engine/infra/server/dependencies.go
index 7f877fd9..e022b661 100644
--- a/engine/infra/server/dependencies.go
+++ b/engine/infra/server/dependencies.go
@@ -19,6 +19,7 @@ import (
"github.com/compozy/compozy/engine/runtime/toolenvstate"
"github.com/compozy/compozy/engine/streaming"
"github.com/compozy/compozy/engine/worker"
+ "github.com/compozy/compozy/engine/worker/embedded"
"github.com/compozy/compozy/engine/workflow"
"github.com/compozy/compozy/pkg/config"
"github.com/compozy/compozy/pkg/logger"
@@ -183,6 +184,11 @@ func (s *Server) setupDependencies() (*appstate.State, []func(), error) {
if err != nil {
return nil, cleanupFuncs, err
}
+ temporalCleanup, err := maybeStartStandaloneTemporal(s.ctx)
+ if err != nil {
+ return nil, cleanupFuncs, err
+ }
+ cleanupFuncs = appendCleanup(cleanupFuncs, temporalCleanup)
deps := appstate.NewBaseDeps(projectConfig, workflows, storeInstance, newTemporalConfig(cfg))
workerInstance, workerCleanup, err := s.maybeStartWorker(deps, resourceStore, cfg, configRegistry)
if err != nil {
@@ -321,6 +327,74 @@ func chooseResourceStore(redisClient *redis.Client, cfg *config.Config) resource
return resources.NewMemoryResourceStore()
}
+func maybeStartStandaloneTemporal(ctx context.Context) (func(), error) {
+ cfg := config.FromContext(ctx)
+ if cfg == nil {
+ return nil, fmt.Errorf("configuration is required to start Temporal")
+ }
+ if cfg.Temporal.Mode != modeStandalone {
+ return nil, nil
+ }
+ embeddedCfg := standaloneEmbeddedConfig(cfg)
+ log := logger.FromContext(ctx)
+ log.Info(
+ "Starting in standalone mode",
+ "database", embeddedCfg.DatabaseFile,
+ "frontend_port", embeddedCfg.FrontendPort,
+ "ui_enabled", embeddedCfg.EnableUI,
+ )
+ log.Warn("Temporal standalone mode is not recommended for production")
+ server, err := embedded.NewServer(ctx, embeddedCfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to prepare embedded Temporal server: %w", err)
+ }
+ if err := server.Start(ctx); err != nil {
+ return nil, fmt.Errorf("failed to start embedded Temporal server: %w", err)
+ }
+ cfg.Temporal.HostPort = server.FrontendAddress()
+ log.Info(
+ "Temporal standalone mode started",
+ "frontend_addr", cfg.Temporal.HostPort,
+ "ui_enabled", embeddedCfg.EnableUI,
+ "ui_port", embeddedCfg.UIPort,
+ )
+ shutdownTimeout := cfg.Server.Timeouts.WorkerShutdown
+ if shutdownTimeout <= 0 {
+ shutdownTimeout = embeddedCfg.StartTimeout
+ }
+ return standaloneTemporalCleanup(ctx, server, shutdownTimeout), nil
+}
+
+func standaloneEmbeddedConfig(cfg *config.Config) *embedded.Config {
+ standalone := cfg.Temporal.Standalone
+ return &embedded.Config{
+ DatabaseFile: standalone.DatabaseFile,
+ FrontendPort: standalone.FrontendPort,
+ BindIP: standalone.BindIP,
+ Namespace: standalone.Namespace,
+ ClusterName: standalone.ClusterName,
+ EnableUI: standalone.EnableUI,
+ RequireUI: standalone.RequireUI,
+ UIPort: standalone.UIPort,
+ LogLevel: standalone.LogLevel,
+ StartTimeout: standalone.StartTimeout,
+ }
+}
+
+func standaloneTemporalCleanup(
+ ctx context.Context,
+ server *embedded.Server,
+ shutdownTimeout time.Duration,
+) func() {
+ return func() {
+ stopCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), shutdownTimeout)
+ defer cancel()
+ if err := server.Stop(stopCtx); err != nil {
+ logger.FromContext(ctx).Warn("Failed to stop embedded Temporal server", "error", err)
+ }
+ }
+}
+
// appendCleanup appends a cleanup function when it is non-nil.
func appendCleanup(cleanups []func(), cleanup func()) []func() {
if cleanup == nil {
diff --git a/engine/worker/embedded/builder.go b/engine/worker/embedded/builder.go
new file mode 100644
index 00000000..1ebe1814
--- /dev/null
+++ b/engine/worker/embedded/builder.go
@@ -0,0 +1,178 @@
+package embedded
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/compozy/compozy/engine/core"
+ "github.com/google/uuid"
+ "go.temporal.io/server/common/cluster"
+ "go.temporal.io/server/common/config"
+ "go.temporal.io/server/common/log"
+ "go.temporal.io/server/common/membership/static"
+ "go.temporal.io/server/common/metrics"
+ sqliteplugin "go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite"
+ "go.temporal.io/server/common/primitives"
+)
+
+const (
+ sqliteStoreName = "sqlite-default"
+ connectProtocol = "tcp"
+)
+
+func buildTemporalConfig(cfg *Config) (*config.Config, error) {
+ metricsPort, err := calculateMetricsPort(cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ sqlCfg := buildSQLiteSQLConfig(cfg)
+ persistence := buildPersistenceConfig(sqlCfg)
+ services := buildServiceConfig(cfg)
+
+ temporalCfg := &config.Config{
+ Global: buildGlobalConfig(cfg, metricsPort),
+ Persistence: persistence,
+ Log: buildLogConfig(cfg),
+ ClusterMetadata: buildClusterMetadata(cfg),
+ DCRedirectionPolicy: config.DCRedirectionPolicy{Policy: "noop"},
+ Services: services,
+ Archival: buildArchivalConfig(),
+ NamespaceDefaults: buildNamespaceDefaultsConfig(),
+ PublicClient: config.PublicClient{
+ HostPort: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ },
+ }
+
+ return temporalCfg, nil
+}
+
+func calculateMetricsPort(cfg *Config) (int, error) {
+ metricsPort := cfg.FrontendPort + 1000
+ if metricsPort > maxPort {
+ return 0, fmt.Errorf("metrics port %d exceeds maximum for frontend port %d", metricsPort, cfg.FrontendPort)
+ }
+ return metricsPort, nil
+}
+
+func buildPersistenceConfig(sqlCfg *config.SQL) config.Persistence {
+ return config.Persistence{
+ DefaultStore: sqliteStoreName,
+ VisibilityStore: sqliteStoreName,
+ NumHistoryShards: 1,
+ DataStores: map[string]config.DataStore{
+ sqliteStoreName: {SQL: sqlCfg},
+ },
+ }
+}
+
+func buildGlobalConfig(cfg *Config, metricsPort int) config.Global {
+ return config.Global{
+ Membership: config.Membership{
+ MaxJoinDuration: cfg.StartTimeout,
+ BroadcastAddress: cfg.BindIP,
+ },
+ Metrics: &metrics.Config{
+ Prometheus: &metrics.PrometheusConfig{
+ ListenAddress: fmt.Sprintf("%s:%d", cfg.BindIP, metricsPort),
+ HandlerPath: "/metrics",
+ },
+ },
+ }
+}
+
+func buildLogConfig(cfg *Config) log.Config {
+ return log.Config{
+ Stdout: true,
+ Level: cfg.LogLevel,
+ Format: "console",
+ }
+}
+
+func buildClusterMetadata(cfg *Config) *cluster.Config {
+ return &cluster.Config{
+ EnableGlobalNamespace: false,
+ FailoverVersionIncrement: 10,
+ MasterClusterName: cfg.ClusterName,
+ CurrentClusterName: cfg.ClusterName,
+ ClusterInformation: map[string]cluster.ClusterInformation{
+ cfg.ClusterName: {
+ Enabled: true,
+ InitialFailoverVersion: 1,
+ RPCAddress: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ ClusterID: uuid.NewString(),
+ },
+ },
+ }
+}
+
+func buildArchivalConfig() config.Archival {
+ return config.Archival{
+ History: config.HistoryArchival{State: "disabled"},
+ Visibility: config.VisibilityArchival{State: "disabled"},
+ }
+}
+
+func buildNamespaceDefaultsConfig() config.NamespaceDefaults {
+ return config.NamespaceDefaults{
+ Archival: config.ArchivalNamespaceDefaults{
+ History: config.HistoryArchivalNamespaceDefaults{State: "disabled"},
+ Visibility: config.VisibilityArchivalNamespaceDefaults{State: "disabled"},
+ },
+ }
+}
+
+func buildSQLiteSQLConfig(cfg *Config) *config.SQL {
+ attrs := core.CloneMap(buildSQLiteConnectAttrs(cfg))
+ return &config.SQL{
+ PluginName: sqliteplugin.PluginName,
+ DatabaseName: cfg.DatabaseFile,
+ ConnectAddr: cfg.BindIP,
+ ConnectProtocol: connectProtocol,
+ ConnectAttributes: attrs,
+ MaxConns: 1,
+ MaxIdleConns: 1,
+ MaxConnLifetime: time.Hour,
+ }
+}
+
+func buildSQLiteConnectAttrs(cfg *Config) map[string]string {
+ if cfg.DatabaseFile == ":memory:" {
+ return map[string]string{
+ "mode": "memory",
+ "cache": "shared",
+ "setup": "true",
+ }
+ }
+ return map[string]string{
+ "cache": "private",
+ "journal_mode": "wal",
+ "synchronous": "2",
+ "setup": "true",
+ }
+}
+
+func buildServiceConfig(cfg *Config) map[string]config.Service {
+ historyPort := cfg.FrontendPort + 1
+ matchingPort := cfg.FrontendPort + 2
+ workerPort := cfg.FrontendPort + 3
+ return map[string]config.Service{
+ "frontend": {RPC: config.RPC{GRPCPort: cfg.FrontendPort, BindOnIP: cfg.BindIP}},
+ "history": {RPC: config.RPC{GRPCPort: historyPort, BindOnIP: cfg.BindIP}},
+ "matching": {RPC: config.RPC{GRPCPort: matchingPort, BindOnIP: cfg.BindIP}},
+ "worker": {RPC: config.RPC{GRPCPort: workerPort, BindOnIP: cfg.BindIP}},
+ }
+}
+
+func buildStaticHosts(cfg *Config) map[primitives.ServiceName]static.Hosts {
+ frontend := fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort)
+ history := fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+1)
+ matching := fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+2)
+ worker := fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+3)
+ return map[primitives.ServiceName]static.Hosts{
+ primitives.FrontendService: static.SingleLocalHost(frontend),
+ primitives.HistoryService: static.SingleLocalHost(history),
+ primitives.MatchingService: static.SingleLocalHost(matching),
+ primitives.WorkerService: static.SingleLocalHost(worker),
+ }
+}
diff --git a/engine/worker/embedded/builder_test.go b/engine/worker/embedded/builder_test.go
new file mode 100644
index 00000000..1ba314f4
--- /dev/null
+++ b/engine/worker/embedded/builder_test.go
@@ -0,0 +1,126 @@
+package embedded
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.temporal.io/server/common/cluster"
+ "go.temporal.io/server/common/primitives"
+)
+
+func TestBuildTemporalConfig(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{
+ DatabaseFile: ":memory:",
+ FrontendPort: 7233,
+ BindIP: "127.0.0.1",
+ Namespace: "default",
+ ClusterName: "cluster",
+ EnableUI: true,
+ UIPort: 8233,
+ LogLevel: "warn",
+ StartTimeout: 45 * time.Second,
+ }
+
+ temporalCfg, err := buildTemporalConfig(cfg)
+ require.NoError(t, err)
+
+ require.NotNil(t, temporalCfg.Persistence.DataStores[sqliteStoreName].SQL)
+ sqlCfg := temporalCfg.Persistence.DataStores[sqliteStoreName].SQL
+ assert.Equal(t, cfg.DatabaseFile, sqlCfg.DatabaseName)
+ assert.Equal(t, cfg.BindIP, sqlCfg.ConnectAddr)
+ assert.Equal(t, connectProtocol, sqlCfg.ConnectProtocol)
+ assert.Equal(t, "sqlite", sqlCfg.PluginName)
+
+ assert.Equal(t, sqliteStoreName, temporalCfg.Persistence.DefaultStore)
+ assert.Equal(t, sqliteStoreName, temporalCfg.Persistence.VisibilityStore)
+ assert.EqualValues(t, 1, temporalCfg.Persistence.NumHistoryShards)
+
+ assert.Contains(t, temporalCfg.Services, "frontend")
+ assert.Equal(t, cfg.FrontendPort, temporalCfg.Services["frontend"].RPC.GRPCPort)
+ assert.Equal(t, cfg.BindIP, temporalCfg.Services["frontend"].RPC.BindOnIP)
+ assert.Equal(t, cfg.FrontendPort+1, temporalCfg.Services["history"].RPC.GRPCPort)
+ assert.Equal(t, cfg.FrontendPort+2, temporalCfg.Services["matching"].RPC.GRPCPort)
+ assert.Equal(t, cfg.FrontendPort+3, temporalCfg.Services["worker"].RPC.GRPCPort)
+
+ assert.Equal(t, "127.0.0.1:7233", temporalCfg.PublicClient.HostPort)
+ assert.Equal(t, "127.0.0.1:8233", temporalCfg.Global.Metrics.Prometheus.ListenAddress)
+ assert.Equal(t, "127.0.0.1:7233", temporalCfg.ClusterMetadata.ClusterInformation[cfg.ClusterName].RPCAddress)
+ assert.Equal(t, cfg.ClusterName, temporalCfg.ClusterMetadata.CurrentClusterName)
+ assert.Equal(t, int64(10), temporalCfg.ClusterMetadata.FailoverVersionIncrement)
+}
+
+func TestBuildStaticHostsConfiguration(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{BindIP: "0.0.0.0", FrontendPort: 7000}
+ hosts := buildStaticHosts(cfg)
+
+ require.Len(t, hosts, 4)
+ assert.Equal(t, "0.0.0.0:7000", hosts[primitives.FrontendService].Self)
+ assert.Equal(t, "0.0.0.0:7001", hosts[primitives.HistoryService].Self)
+ assert.Equal(t, "0.0.0.0:7002", hosts[primitives.MatchingService].Self)
+ assert.Equal(t, "0.0.0.0:7003", hosts[primitives.WorkerService].Self)
+}
+
+func TestBuildSQLiteConnectAttrsModes(t *testing.T) {
+ t.Parallel()
+
+ memoryAttrs := buildSQLiteConnectAttrs(&Config{DatabaseFile: ":memory:"})
+ assert.Equal(t, "memory", memoryAttrs["mode"])
+ assert.Equal(t, "shared", memoryAttrs["cache"])
+
+ fileAttrs := buildSQLiteConnectAttrs(&Config{DatabaseFile: "temporal.db"})
+ assert.Equal(t, "private", fileAttrs["cache"])
+ assert.Equal(t, "wal", fileAttrs["journal_mode"])
+ assert.Equal(t, "2", fileAttrs["synchronous"])
+ assert.Equal(t, "true", fileAttrs["setup"])
+}
+
+func TestBuildTemporalConfigMetricsPort(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{
+ DatabaseFile: ":memory:",
+ FrontendPort: maxPort - 999,
+ BindIP: "127.0.0.1",
+ Namespace: "default",
+ ClusterName: "cluster",
+ EnableUI: true,
+ UIPort: 8233,
+ LogLevel: "info",
+ StartTimeout: time.Second,
+ }
+
+ _, err := buildTemporalConfig(cfg)
+ require.Error(t, err)
+}
+
+func TestBuildTemporalConfigClusterMetadata(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{
+ DatabaseFile: ":memory:",
+ FrontendPort: 7233,
+ BindIP: "127.0.0.1",
+ Namespace: "default",
+ ClusterName: "cluster",
+ EnableUI: true,
+ UIPort: 8233,
+ LogLevel: "warn",
+ StartTimeout: time.Second,
+ }
+
+ temporalCfg, err := buildTemporalConfig(cfg)
+ require.NoError(t, err)
+
+ info, ok := temporalCfg.ClusterMetadata.ClusterInformation[cfg.ClusterName]
+ require.True(t, ok)
+ assert.True(t, info.Enabled)
+ assert.Equal(t, int64(1), info.InitialFailoverVersion)
+ assert.NotEmpty(t, info.ClusterID)
+ assert.IsType(t, &cluster.Config{}, temporalCfg.ClusterMetadata)
+}
diff --git a/engine/worker/embedded/config.go b/engine/worker/embedded/config.go
new file mode 100644
index 00000000..2e06f452
--- /dev/null
+++ b/engine/worker/embedded/config.go
@@ -0,0 +1,193 @@
+package embedded
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+const (
+ defaultDatabaseFile = ":memory:"
+ defaultFrontendPort = 7233
+ defaultBindIP = "127.0.0.1"
+ defaultNamespace = "default"
+ defaultClusterName = "compozy-standalone"
+ defaultEnableUI = true
+ defaultUIPort = 8233
+ defaultLogLevel = "warn"
+ defaultStartTimeout = 30 * time.Second
+ maxServicePortOffset = 3
+ maxPort = 65535
+)
+
+var allowedLogLevels = map[string]struct{}{
+ "debug": {},
+ "info": {},
+ "warn": {},
+ "error": {},
+}
+
+// Config holds embedded Temporal server configuration.
+type Config struct {
+ // DatabaseFile specifies SQLite database location.
+ // Use ":memory:" for ephemeral in-memory storage.
+ // Use file path for persistent storage across restarts.
+ DatabaseFile string
+
+ // FrontendPort is the gRPC port for the frontend service.
+ FrontendPort int
+
+ // BindIP is the IP address to bind all services to.
+ BindIP string
+
+ // Namespace is the default namespace to create on startup.
+ Namespace string
+
+ // ClusterName is the Temporal cluster name.
+ ClusterName string
+
+ // EnableUI enables the Temporal Web UI server.
+ // Set to true to enable the UI server on the specified UIPort.
+ EnableUI bool
+
+ // RequireUI enforces UI availability; Start returns an error if the UI fails to launch.
+ RequireUI bool
+
+ // UIPort is the HTTP port for the Web UI.
+ UIPort int
+
+ // LogLevel controls server logging verbosity.
+ LogLevel string
+
+ // StartTimeout is the maximum time to wait for server startup.
+ StartTimeout time.Duration
+}
+
+func applyDefaults(cfg *Config) {
+ if cfg == nil {
+ return
+ }
+ if cfg.DatabaseFile == "" {
+ cfg.DatabaseFile = defaultDatabaseFile
+ }
+ if cfg.FrontendPort == 0 {
+ cfg.FrontendPort = defaultFrontendPort
+ }
+ if cfg.BindIP == "" {
+ cfg.BindIP = defaultBindIP
+ }
+ if cfg.Namespace == "" {
+ cfg.Namespace = defaultNamespace
+ }
+ if cfg.ClusterName == "" {
+ cfg.ClusterName = defaultClusterName
+ }
+ if cfg.UIPort == 0 {
+ cfg.UIPort = defaultUIPort
+ }
+ if cfg.LogLevel == "" {
+ cfg.LogLevel = defaultLogLevel
+ }
+ if cfg.StartTimeout == 0 {
+ cfg.StartTimeout = defaultStartTimeout
+ }
+}
+
+func validateConfig(cfg *Config) error {
+ if cfg == nil {
+ return errors.New("config is nil")
+ }
+ if err := validateDatabaseFile(cfg.DatabaseFile); err != nil {
+ return err
+ }
+ if err := validateFrontendPort(cfg.FrontendPort); err != nil {
+ return err
+ }
+ if err := validatePort("ui_port", cfg.UIPort, cfg.EnableUI); err != nil {
+ return err
+ }
+ if cfg.RequireUI && !cfg.EnableUI {
+ return errors.New("require_ui cannot be set when enable_ui is false")
+ }
+ if err := validateBindIP(cfg.BindIP); err != nil {
+ return err
+ }
+ if cfg.Namespace == "" {
+ return errors.New("namespace is required")
+ }
+ if cfg.ClusterName == "" {
+ return errors.New("cluster name is required")
+ }
+ if err := validateLogLevel(cfg.LogLevel); err != nil {
+ return err
+ }
+ if cfg.StartTimeout <= 0 {
+ return errors.New("start timeout must be positive")
+ }
+ return nil
+}
+
+func validateDatabaseFile(path string) error {
+ if path == "" {
+ return errors.New("database file is required")
+ }
+ if path == ":memory:" {
+ return nil
+ }
+ abs, err := filepath.Abs(path)
+ if err != nil {
+ return fmt.Errorf("resolve database file: %w", err)
+ }
+ dir := filepath.Dir(abs)
+ info, err := os.Stat(dir)
+ if err != nil {
+ return fmt.Errorf("database directory %q not accessible: %w", dir, err)
+ }
+ if !info.IsDir() {
+ return fmt.Errorf("database directory %q is not a directory", dir)
+ }
+ return nil
+}
+
+func validateFrontendPort(port int) error {
+ if err := validatePort("frontend_port", port, true); err != nil {
+ return err
+ }
+ if port+maxServicePortOffset > maxPort {
+ return fmt.Errorf("frontend port %d reserves out-of-range service port", port)
+ }
+ return nil
+}
+
+func validatePort(field string, port int, required bool) error {
+ if !required && port == 0 {
+ return nil
+ }
+ if port <= 0 || port > maxPort {
+ return fmt.Errorf("%s must be between 1 and %d", field, maxPort)
+ }
+ return nil
+}
+
+func validateBindIP(ip string) error {
+ if ip == "" {
+ return errors.New("bind IP is required")
+ }
+ if parsed := net.ParseIP(ip); parsed == nil {
+ return fmt.Errorf("invalid bind IP %q", ip)
+ }
+ return nil
+}
+
+func validateLogLevel(level string) error {
+ if level == "" {
+ return errors.New("log level is required")
+ }
+ if _, ok := allowedLogLevels[level]; !ok {
+ return fmt.Errorf("invalid log level %q", level)
+ }
+ return nil
+}
diff --git a/engine/worker/embedded/config_test.go b/engine/worker/embedded/config_test.go
new file mode 100644
index 00000000..3eeeda46
--- /dev/null
+++ b/engine/worker/embedded/config_test.go
@@ -0,0 +1,172 @@
+package embedded
+
+import (
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.temporal.io/server/common/primitives"
+)
+
+func TestValidateConfig(t *testing.T) {
+ t.Parallel()
+
+ valid := Config{
+ DatabaseFile: ":memory:",
+ FrontendPort: 7233,
+ BindIP: "127.0.0.1",
+ Namespace: "default",
+ ClusterName: "cluster",
+ EnableUI: true,
+ UIPort: 8233,
+ LogLevel: "info",
+ StartTimeout: time.Second,
+ }
+
+ cases := []struct {
+ name string
+ mutate func(cfg *Config)
+ wantErr string
+ }{
+ {
+ name: "Should validate a correct configuration",
+ mutate: func(_ *Config) {},
+ },
+ {
+ name: "Should reject invalid frontend port",
+ mutate: func(cfg *Config) {
+ cfg.FrontendPort = -1
+ },
+ wantErr: "frontend_port",
+ },
+ {
+ name: "Should reject service port overflow",
+ mutate: func(cfg *Config) {
+ cfg.FrontendPort = 65534
+ },
+ wantErr: "out-of-range",
+ },
+ {
+ name: "Should reject invalid ui port",
+ mutate: func(cfg *Config) {
+ cfg.UIPort = 0
+ },
+ wantErr: "ui_port",
+ },
+ {
+ name: "Should reject invalid bind ip",
+ mutate: func(cfg *Config) {
+ cfg.BindIP = "not-an-ip"
+ },
+ wantErr: "invalid bind IP",
+ },
+ {
+ name: "Should reject invalid log level",
+ mutate: func(cfg *Config) {
+ cfg.LogLevel = "verbose"
+ },
+ wantErr: "invalid log level",
+ },
+ {
+ name: "Should reject invalid database path",
+ mutate: func(cfg *Config) {
+ cfg.DatabaseFile = filepath.Join(string(filepath.Separator), "does", "not", "exist", "temporal.db")
+ },
+ wantErr: "database directory",
+ },
+ {
+ name: "Should require namespace",
+ mutate: func(cfg *Config) {
+ cfg.Namespace = ""
+ },
+ wantErr: "namespace is required",
+ },
+ {
+ name: "Should require cluster name",
+ mutate: func(cfg *Config) {
+ cfg.ClusterName = ""
+ },
+ wantErr: "cluster name is required",
+ },
+ {
+ name: "Should reject invalid start timeout",
+ mutate: func(cfg *Config) {
+ cfg.StartTimeout = 0
+ },
+ wantErr: "start timeout",
+ },
+ {
+ name: "Should allow zero port when ui disabled",
+ mutate: func(cfg *Config) {
+ cfg.EnableUI = false
+ cfg.UIPort = 0
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := valid
+ tc.mutate(&cfg)
+ err := validateConfig(&cfg)
+ if tc.wantErr == "" {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tc.wantErr)
+ }
+ })
+ }
+}
+
+func TestApplyDefaults(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{}
+ applyDefaults(cfg)
+
+ assert.Equal(t, defaultDatabaseFile, cfg.DatabaseFile)
+ assert.Equal(t, defaultFrontendPort, cfg.FrontendPort)
+ assert.Equal(t, defaultBindIP, cfg.BindIP)
+ assert.Equal(t, defaultNamespace, cfg.Namespace)
+ assert.Equal(t, defaultClusterName, cfg.ClusterName)
+ assert.Equal(t, defaultUIPort, cfg.UIPort)
+ assert.Equal(t, defaultLogLevel, cfg.LogLevel)
+ assert.Equal(t, defaultStartTimeout, cfg.StartTimeout)
+ // EnableUI is not set by applyDefaults - callers must set explicitly or rely on application-level defaults
+}
+
+func TestBuildSQLiteConnectAttrs(t *testing.T) {
+ t.Parallel()
+
+ memoryAttrs := buildSQLiteConnectAttrs(&Config{DatabaseFile: ":memory:"})
+ require.Equal(t, map[string]string{
+ "mode": "memory",
+ "cache": "shared",
+ "setup": "true",
+ }, memoryAttrs)
+
+ fileAttrs := buildSQLiteConnectAttrs(&Config{DatabaseFile: "temporal.db"})
+ require.Equal(t, map[string]string{
+ "cache": "private",
+ "journal_mode": "wal",
+ "synchronous": "2",
+ "setup": "true",
+ }, fileAttrs)
+}
+
+func TestBuildStaticHosts(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{BindIP: "127.0.0.1", FrontendPort: 7233}
+ hosts := buildStaticHosts(cfg)
+
+ assert.Equal(t, "127.0.0.1:7233", hosts[primitives.FrontendService].Self)
+ assert.Equal(t, []string{"127.0.0.1:7233"}, hosts[primitives.FrontendService].All)
+ assert.Equal(t, "127.0.0.1:7234", hosts[primitives.HistoryService].Self)
+ assert.Equal(t, "127.0.0.1:7235", hosts[primitives.MatchingService].Self)
+ assert.Equal(t, "127.0.0.1:7236", hosts[primitives.WorkerService].Self)
+}
diff --git a/engine/worker/embedded/namespace.go b/engine/worker/embedded/namespace.go
new file mode 100644
index 00000000..e9f0dded
--- /dev/null
+++ b/engine/worker/embedded/namespace.go
@@ -0,0 +1,59 @@
+package embedded
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/compozy/compozy/engine/core"
+ "github.com/compozy/compozy/pkg/logger"
+ enumspb "go.temporal.io/api/enums/v1"
+ "go.temporal.io/server/common/config"
+ sqliteschema "go.temporal.io/server/schema/sqlite"
+)
+
+func createNamespace(ctx context.Context, serverCfg *config.Config, embeddedCfg *Config) error {
+ log := logger.FromContext(ctx)
+ if serverCfg == nil {
+ return errors.New("temporal config is nil")
+ }
+ if embeddedCfg == nil {
+ return errors.New("embedded config is nil")
+ }
+
+ storeName := serverCfg.Persistence.DefaultStore
+ datastore, ok := serverCfg.Persistence.DataStores[storeName]
+ if !ok || datastore.SQL == nil {
+ return fmt.Errorf("sql datastore %q is not configured", storeName)
+ }
+
+ sqlCfg := cloneSQLConfig(datastore.SQL)
+ namespace, err := sqliteschema.NewNamespaceConfig(
+ embeddedCfg.ClusterName,
+ embeddedCfg.Namespace,
+ false,
+ map[string]enumspb.IndexedValueType{},
+ )
+ if err != nil {
+ return fmt.Errorf("build namespace config failed: %w", err)
+ }
+ if err := sqliteschema.CreateNamespaces(sqlCfg, namespace); err != nil {
+ e := strings.ToLower(err.Error())
+ if strings.Contains(e, "already exists") {
+ log.Debug("temporal namespace already exists; continuing", "namespace", embeddedCfg.Namespace)
+ return nil
+ }
+ return fmt.Errorf("create namespace %q failed: %w", embeddedCfg.Namespace, err)
+ }
+ return nil
+}
+
+func cloneSQLConfig(src *config.SQL) *config.SQL {
+ if src == nil {
+ return nil
+ }
+ clone := *src
+ clone.ConnectAttributes = core.CloneMap(src.ConnectAttributes)
+ return &clone
+}
diff --git a/engine/worker/embedded/namespace_test.go b/engine/worker/embedded/namespace_test.go
new file mode 100644
index 00000000..cd03278c
--- /dev/null
+++ b/engine/worker/embedded/namespace_test.go
@@ -0,0 +1,76 @@
+package embedded
+
+import (
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "go.temporal.io/server/common/log"
+ "go.temporal.io/server/common/metrics"
+ persistenceSQL "go.temporal.io/server/common/persistence/sql"
+ "go.temporal.io/server/common/persistence/sql/sqlplugin"
+ "go.temporal.io/server/common/resolver"
+)
+
+func TestCreateNamespace(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Should create namespace successfully", func(t *testing.T) {
+ cfg := newNamespaceTestConfig(t)
+ require.NoError(t, validateConfig(cfg))
+
+ temporalCfg, err := buildTemporalConfig(cfg)
+ require.NoError(t, err)
+
+ require.NoError(t, createNamespace(t.Context(), temporalCfg, cfg))
+ })
+
+ t.Run("Should persist namespace to database", func(t *testing.T) {
+ cfg := newNamespaceTestConfig(t)
+ temporalCfg, err := buildTemporalConfig(cfg)
+ require.NoError(t, err)
+ require.NoError(t, createNamespace(t.Context(), temporalCfg, cfg))
+
+ sqlCfg := temporalCfg.Persistence.DataStores[temporalCfg.Persistence.DefaultStore].SQL
+ db, err := persistenceSQL.NewSQLDB(
+ sqlplugin.DbKindUnknown,
+ sqlCfg,
+ resolver.NewNoopResolver(),
+ log.NewNoopLogger(),
+ metrics.NoopMetricsHandler,
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ _ = db.Close()
+ })
+
+ ctx := t.Context()
+ rows, err := db.SelectFromNamespace(ctx, sqlplugin.NamespaceFilter{Name: &cfg.Namespace})
+ require.NoError(t, err)
+ require.Len(t, rows, 1)
+ })
+
+ t.Run("Should be idempotent when namespace already exists", func(t *testing.T) {
+ cfg := newNamespaceTestConfig(t)
+ temporalCfg, err := buildTemporalConfig(cfg)
+ require.NoError(t, err)
+ require.NoError(t, createNamespace(t.Context(), temporalCfg, cfg))
+ require.NoError(t, createNamespace(t.Context(), temporalCfg, cfg))
+ })
+}
+
+func newNamespaceTestConfig(t *testing.T) *Config {
+ t.Helper()
+ return &Config{
+ DatabaseFile: filepath.Join(t.TempDir(), "temporal.db"),
+ FrontendPort: 7400,
+ BindIP: "127.0.0.1",
+ Namespace: "standalone",
+ ClusterName: "cluster",
+ EnableUI: true,
+ UIPort: 8300,
+ LogLevel: "info",
+ StartTimeout: 15 * time.Second,
+ }
+}
diff --git a/engine/worker/embedded/server.go b/engine/worker/embedded/server.go
new file mode 100644
index 00000000..2ebe7fae
--- /dev/null
+++ b/engine/worker/embedded/server.go
@@ -0,0 +1,320 @@
+package embedded
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "os"
+ "strconv"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/compozy/compozy/pkg/logger"
+ "go.temporal.io/server/common/log"
+ "go.temporal.io/server/temporal"
+)
+
+const (
+ readyPollInterval = 100 * time.Millisecond
+ readyDialTimeout = 50 * time.Millisecond
+)
+
+var (
+ errNilContext = errors.New("context is required")
+ errAlreadyStarted = errors.New("embedded temporal server already started")
+)
+
+// Server wraps an embedded Temporal server instance.
+type Server struct {
+ mu sync.Mutex // protects state fields like started
+ opMu sync.Mutex // serializes start/stop operations
+ server temporal.Server
+ config *Config
+ frontendAddr string
+ uiServer *UIServer
+ started bool
+}
+
+// NewServer creates but does not start an embedded Temporal server.
+// Validates configuration, prepares persistence, and instantiates Temporal services.
+func NewServer(ctx context.Context, cfg *Config) (*Server, error) {
+ if ctx == nil {
+ return nil, errNilContext
+ }
+ if err := validateConfig(cfg); err != nil {
+ return nil, fmt.Errorf("invalid config: %w", err)
+ }
+ applyDefaults(cfg)
+
+ server, frontendAddr, err := buildEmbeddedTemporalServer(ctx, cfg)
+ if err != nil {
+ return nil, err
+ }
+ uiSrv := newUIServer(cfg)
+
+ s := &Server{
+ server: server,
+ config: cfg,
+ frontendAddr: frontendAddr,
+ uiServer: uiSrv,
+ }
+
+ logger.FromContext(ctx).Debug(
+ "Embedded Temporal server prepared",
+ "frontend_addr", s.frontendAddr,
+ "database", cfg.DatabaseFile,
+ "cluster", cfg.ClusterName,
+ )
+ if uiSrv == nil {
+ logger.FromContext(ctx).Debug("Temporal UI disabled for embedded server")
+ } else {
+ logger.FromContext(ctx).Debug("Temporal UI prepared", "ui_addr", uiSrv.address)
+ }
+
+ return s, nil
+}
+
+func buildEmbeddedTemporalServer(ctx context.Context, cfg *Config) (temporal.Server, string, error) {
+ serverConfig, err := buildTemporalConfig(cfg)
+ if err != nil {
+ return nil, "", fmt.Errorf("build temporal config: %w", err)
+ }
+ if err := createNamespace(ctx, serverConfig, cfg); err != nil {
+ return nil, "", fmt.Errorf("create namespace: %w", err)
+ }
+
+ if err := ensurePortsAvailable(ctx, cfg.BindIP, servicePorts(cfg)); err != nil {
+ return nil, "", err
+ }
+
+ temporalLogger := log.NewZapLogger(log.BuildZapLogger(buildLogConfig(cfg)))
+ server, err := temporal.NewServer(
+ temporal.WithConfig(serverConfig),
+ temporal.ForServices(temporal.DefaultServices),
+ temporal.WithStaticHosts(buildStaticHosts(cfg)),
+ temporal.WithLogger(temporalLogger),
+ )
+ if err != nil {
+ return nil, "", fmt.Errorf("create temporal server: %w", err)
+ }
+
+ return server, net.JoinHostPort(cfg.BindIP, strconv.Itoa(cfg.FrontendPort)), nil
+}
+
+// Start boots the embedded Temporal server and waits for readiness.
+func (s *Server) Start(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+
+ log := logger.FromContext(ctx)
+
+ s.opMu.Lock()
+ defer s.opMu.Unlock()
+
+ s.mu.Lock()
+ if s.started {
+ s.mu.Unlock()
+ return errAlreadyStarted
+ }
+ s.started = true
+ s.mu.Unlock()
+
+ duration, err := s.startCore(ctx, log)
+ if err != nil {
+ s.setStarted(false)
+ return err
+ }
+
+ if err := s.startUIServer(ctx, log); err != nil {
+ s.setStarted(false)
+ return err
+ }
+
+ log.Info(
+ "Embedded Temporal server started",
+ "frontend_addr", s.frontendAddr,
+ "duration", duration,
+ )
+
+ return nil
+}
+
+// Stop gracefully shuts down the embedded Temporal server.
+func (s *Server) Stop(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+
+ s.opMu.Lock()
+ defer s.opMu.Unlock()
+
+ s.mu.Lock()
+ if !s.started {
+ s.mu.Unlock()
+ return nil
+ }
+ s.started = false
+ s.mu.Unlock()
+
+ stopStart := time.Now()
+ log := logger.FromContext(ctx)
+ log.Info("Stopping embedded Temporal server", "frontend_addr", s.frontendAddr)
+
+ if s.uiServer != nil {
+ if err := s.uiServer.Stop(ctx); err != nil {
+ log.Warn("Failed to stop Temporal UI server", "error", err)
+ }
+ }
+
+ if err := s.server.Stop(); err != nil {
+ return fmt.Errorf("stop temporal server: %w", err)
+ }
+
+ log.Info(
+ "Embedded Temporal server stopped",
+ "frontend_addr", s.frontendAddr,
+ "duration", time.Since(stopStart),
+ )
+
+ return nil
+}
+
+// FrontendAddress returns the gRPC address for the Temporal frontend service.
+func (s *Server) FrontendAddress() string {
+ return s.frontendAddr
+}
+
+// waitForReady polls the frontend service until ready or the context ends.
+func (s *Server) waitForReady(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+ dialer := &net.Dialer{Timeout: readyDialTimeout}
+ ticker := time.NewTicker(readyPollInterval)
+ defer ticker.Stop()
+
+ host, port, err := net.SplitHostPort(s.frontendAddr)
+ if err != nil {
+ return fmt.Errorf("parse frontend address %q: %w", s.frontendAddr, err)
+ }
+ target := net.JoinHostPort(dialHost(host), port)
+
+ for {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("temporal frontend %s not ready before deadline: %w", target, ctx.Err())
+ case <-ticker.C:
+ conn, err := dialer.DialContext(ctx, "tcp", target)
+ if err == nil {
+ _ = conn.Close()
+ return nil
+ }
+ }
+ }
+}
+
+func ensurePortsAvailable(ctx context.Context, bindIP string, ports []int) error {
+ dialer := &net.Dialer{Timeout: readyDialTimeout}
+ for _, port := range ports {
+ addr := net.JoinHostPort(dialHost(bindIP), strconv.Itoa(port))
+ conn, err := dialer.DialContext(ctx, "tcp", addr)
+ if err == nil {
+ _ = conn.Close()
+ return fmt.Errorf(
+ "embedded temporal port %d is already in use on %s; adjust configuration or stop the conflicting service",
+ port,
+ bindIP,
+ )
+ }
+ if !isConnRefused(err) {
+ return fmt.Errorf("verify port %d on %s: %w", port, bindIP, err)
+ }
+ }
+ return nil
+}
+
+func isConnRefused(err error) bool {
+ var opErr *net.OpError
+ if errors.As(err, &opErr) {
+ if errors.Is(opErr.Err, syscall.ECONNREFUSED) {
+ return true
+ }
+ var sysErr *os.SyscallError
+ if errors.As(opErr.Err, &sysErr) {
+ return errors.Is(sysErr.Err, syscall.ECONNREFUSED)
+ }
+ }
+ return false
+}
+
+func servicePorts(cfg *Config) []int {
+ ports := []int{
+ cfg.FrontendPort,
+ cfg.FrontendPort + 1,
+ cfg.FrontendPort + 2,
+ cfg.FrontendPort + 3,
+ }
+ if cfg.EnableUI {
+ ports = append(ports, cfg.UIPort)
+ }
+ return ports
+}
+
+func dialHost(bindIP string) string {
+ switch bindIP {
+ case "", "0.0.0.0":
+ return "127.0.0.1"
+ case "::", "[::]":
+ return "::1"
+ default:
+ return bindIP
+ }
+}
+
+func (s *Server) setStarted(started bool) {
+ s.mu.Lock()
+ s.started = started
+ s.mu.Unlock()
+}
+
+func (s *Server) startCore(ctx context.Context, log logger.Logger) (time.Duration, error) {
+ startCtx, cancel := context.WithTimeout(ctx, s.config.StartTimeout)
+ defer cancel()
+
+ startTime := time.Now()
+ if err := s.server.Start(); err != nil {
+ return 0, fmt.Errorf("start temporal server: %w", err)
+ }
+
+ if err := s.waitForReady(startCtx); err != nil {
+ stopErr := s.server.Stop()
+ if stopErr != nil {
+ log.Error("Failed to stop Temporal server after startup error", "error", stopErr)
+ }
+ return 0, fmt.Errorf("wait for ready: %w", err)
+ }
+
+ return time.Since(startTime), nil
+}
+
+func (s *Server) startUIServer(ctx context.Context, log logger.Logger) error {
+ if s.uiServer == nil {
+ return nil
+ }
+
+ if err := s.uiServer.Start(ctx); err != nil {
+ if s.config.RequireUI {
+ stopErr := s.server.Stop()
+ if stopErr != nil {
+ log.Error("Failed to stop Temporal server after UI startup error", "error", stopErr)
+ }
+ return fmt.Errorf("start temporal ui server: %w", err)
+ }
+ log.Warn("Failed to start Temporal UI server", "error", err)
+ }
+
+ return nil
+}
diff --git a/engine/worker/embedded/server_test.go b/engine/worker/embedded/server_test.go
new file mode 100644
index 00000000..1e45be7c
--- /dev/null
+++ b/engine/worker/embedded/server_test.go
@@ -0,0 +1,200 @@
+package embedded
+
+import (
+ "context"
+ "net"
+ "path/filepath"
+ "strconv"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/compozy/compozy/pkg/logger"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var nextTestPort uint32 = 54000
+
+func TestNewServer(t *testing.T) {
+ t.Run("Should create server with valid config", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+
+ srv, err := NewServer(ctx, cfg)
+ require.NoError(t, err)
+ require.NotNil(t, srv)
+ assert.Equal(t, cfg.DatabaseFile, srv.config.DatabaseFile)
+ assert.Equal(t, cfg.ClusterName, srv.config.ClusterName)
+ assert.Equal(t, cfg.Namespace, srv.config.Namespace)
+ assert.False(t, srv.started)
+ assert.Equal(t, srv.frontendAddr, srv.FrontendAddress())
+ })
+
+ t.Run("Should reject invalid config", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+ cfg.FrontendPort = -1
+
+ srv, err := NewServer(ctx, cfg)
+ require.Error(t, err)
+ assert.Nil(t, srv)
+ })
+}
+
+func TestServerStartStop(t *testing.T) {
+ t.Run("Should start server successfully", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+
+ srv := newServerForTest(ctx, t, cfg)
+ require.NoError(t, srv.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, srv.Stop(ctx))
+ })
+
+ dialer := &net.Dialer{Timeout: 500 * time.Millisecond}
+ conn, err := dialer.DialContext(ctx, "tcp", srv.FrontendAddress())
+ require.NoError(t, err)
+ require.NoError(t, conn.Close())
+ })
+
+ t.Run("Should stop server gracefully", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+
+ srv := newServerForTest(ctx, t, cfg)
+ require.NoError(t, srv.Start(ctx))
+
+ stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+ require.NoError(t, srv.Stop(stopCtx))
+
+ dialer := &net.Dialer{Timeout: 200 * time.Millisecond}
+ _, err := dialer.DialContext(ctx, "tcp", srv.FrontendAddress())
+ require.Error(t, err)
+ })
+
+ t.Run("Should timeout if server doesn't start", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+ cfg.StartTimeout = time.Nanosecond
+
+ srv := newServerForTest(ctx, t, cfg)
+ err := srv.Start(ctx)
+ require.Error(t, err)
+ assert.ErrorContains(t, err, "wait for ready")
+ assert.ErrorIs(t, err, context.DeadlineExceeded)
+ })
+
+ t.Run("Should handle port conflicts", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ listener, port := reservePort(t)
+ defer func() { require.NoError(t, listener.Close()) }()
+
+ cfg := newTestConfig(t)
+ cfg.FrontendPort = port
+
+ srv, err := NewServer(ctx, cfg)
+ assert.Nil(t, srv)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "port")
+ assert.Contains(t, err.Error(), strconv.Itoa(port))
+ })
+
+ t.Run("Should fail start when UI required and port unavailable", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+ cfg.EnableUI = true
+ cfg.RequireUI = true
+ reserve, uiPort := reservePort(t)
+ require.NoError(t, reserve.Close())
+ cfg.UIPort = uiPort
+ require.NoError(t, ensureUIPortAvailable(t.Context(), cfg.BindIP, cfg.UIPort))
+
+ srv := newServerForTest(ctx, t, cfg)
+
+ listenCtx, cancel := context.WithTimeout(ctx, time.Second)
+ listener, err := (&net.ListenConfig{}).Listen(
+ listenCtx,
+ "tcp",
+ net.JoinHostPort(cfg.BindIP, strconv.Itoa(cfg.UIPort)),
+ )
+ cancel()
+ require.NoError(t, err)
+ defer func() { require.NoError(t, listener.Close()) }()
+
+ err = srv.Start(ctx)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "temporal ui port")
+
+ stopCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+ require.NoError(t, srv.Stop(stopCtx))
+ })
+
+ t.Run("Should wait for ready state", func(t *testing.T) {
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := newTestConfig(t)
+
+ srv := newServerForTest(ctx, t, cfg)
+ require.NoError(t, srv.Start(ctx))
+ t.Cleanup(func() {
+ require.NoError(t, srv.Stop(ctx))
+ })
+
+ waitCtx, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ require.NoError(t, srv.waitForReady(waitCtx))
+ })
+}
+
+func newServerForTest(ctx context.Context, t *testing.T, cfg *Config) *Server {
+ t.Helper()
+ srv, err := NewServer(ctx, cfg)
+ require.NoError(t, err)
+ return srv
+}
+
+func newTestConfig(t *testing.T) *Config {
+ t.Helper()
+ return &Config{
+ DatabaseFile: filepath.Join(t.TempDir(), "temporal.db"),
+ FrontendPort: randomPort(t),
+ BindIP: "127.0.0.1",
+ Namespace: "default",
+ ClusterName: "cluster-" + t.Name(),
+ EnableUI: false,
+ UIPort: 0,
+ LogLevel: "error",
+ StartTimeout: 10 * time.Second,
+ }
+}
+
+func randomPort(t *testing.T) int {
+ t.Helper()
+ for attempt := 0; attempt < 512; attempt++ {
+ base := atomic.AddUint32(&nextTestPort, 5)
+ port := int(54000 + base%5000)
+ if port+maxServicePortOffset > maxPort {
+ atomic.StoreUint32(&nextTestPort, 0)
+ continue
+ }
+ ports := []int{port, port + 1, port + 2, port + 3}
+ if err := ensurePortsAvailable(t.Context(), "127.0.0.1", ports); err != nil {
+ continue
+ }
+ return port
+ }
+ t.Fatalf("failed to allocate tcp port after multiple attempts")
+ return 0
+}
+
+func reservePort(t *testing.T) (net.Listener, int) {
+ t.Helper()
+ var lc net.ListenConfig
+ listener, err := lc.Listen(t.Context(), "tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ addr := listener.Addr().(*net.TCPAddr)
+ return listener, addr.Port
+}
diff --git a/engine/worker/embedded/ui.go b/engine/worker/embedded/ui.go
new file mode 100644
index 00000000..ee89c18c
--- /dev/null
+++ b/engine/worker/embedded/ui.go
@@ -0,0 +1,194 @@
+package embedded
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/compozy/compozy/pkg/logger"
+ uiserver "github.com/temporalio/ui-server/v2/server"
+ uiconfig "github.com/temporalio/ui-server/v2/server/config"
+ "github.com/temporalio/ui-server/v2/server/server_options"
+)
+
+// UIServer manages the Temporal Web UI lifecycle for the embedded server.
+type UIServer struct {
+ mu sync.Mutex
+ server *uiserver.Server
+ config *Config
+ address string
+ bindIP string
+ uiPort int
+ temporalAddr string
+ runErrCh chan error
+ started bool
+}
+
+// newUIServer constructs a UIServer when UI support is enabled.
+func newUIServer(cfg *Config) *UIServer {
+ if cfg == nil || !cfg.EnableUI || cfg.UIPort <= 0 {
+ return nil
+ }
+
+ uiCfg := &uiconfig.Config{
+ TemporalGRPCAddress: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ Host: cfg.BindIP,
+ Port: cfg.UIPort,
+ EnableUI: true,
+ DefaultNamespace: cfg.Namespace,
+ HideLogs: true,
+ }
+
+ srv := uiserver.NewServer(server_options.WithConfigProvider(uiCfg))
+
+ return &UIServer{
+ server: srv,
+ config: cfg,
+ address: net.JoinHostPort(cfg.BindIP, strconv.Itoa(cfg.UIPort)),
+ bindIP: cfg.BindIP,
+ uiPort: cfg.UIPort,
+ temporalAddr: uiCfg.TemporalGRPCAddress,
+ }
+}
+
+// Start launches the Temporal Web UI and waits until it becomes reachable.
+func (s *UIServer) Start(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+
+ s.mu.Lock()
+ if s.started || s.runErrCh != nil {
+ s.mu.Unlock()
+ return errAlreadyStarted
+ }
+ runErrCh := make(chan error, 1)
+ s.runErrCh = runErrCh
+ s.mu.Unlock()
+
+ if err := ensureUIPortAvailable(ctx, s.bindIP, s.uiPort); err != nil {
+ s.mu.Lock()
+ s.runErrCh = nil
+ s.mu.Unlock()
+ return err
+ }
+
+ log := logger.FromContext(ctx)
+ log.Info(
+ "Starting Temporal UI server",
+ "address", s.address,
+ "frontend_addr", s.temporalAddr,
+ )
+
+ go func(ch chan<- error) {
+ if err := s.server.Start(); err != nil {
+ ch <- fmt.Errorf("ui server exited: %w", err)
+ }
+ close(ch)
+ }(runErrCh)
+
+ if err := waitForHTTPReady(ctx, s.address, runErrCh); err != nil {
+ s.server.Stop()
+ s.mu.Lock()
+ s.runErrCh = nil
+ s.mu.Unlock()
+ return fmt.Errorf("wait for ui ready: %w", err)
+ }
+
+ s.mu.Lock()
+ s.started = true
+ s.mu.Unlock()
+
+ log.Info("Temporal UI server started", "address", s.address)
+ return nil
+}
+
+// Stop gracefully shuts down the Temporal Web UI server.
+func (s *UIServer) Stop(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+
+ s.mu.Lock()
+ if !s.started && s.runErrCh == nil {
+ s.mu.Unlock()
+ return nil
+ }
+ runErrCh := s.runErrCh
+ s.runErrCh = nil
+ s.started = false
+ s.mu.Unlock()
+
+ logger.FromContext(ctx).Info("Stopping Temporal UI server", "address", s.address)
+
+ done := make(chan struct{})
+ go func() {
+ s.server.Stop()
+ close(done)
+ }()
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-done:
+ if runErrCh != nil {
+ select {
+ case <-runErrCh:
+ default:
+ }
+ }
+ logger.FromContext(ctx).Info("Temporal UI server stopped", "address", s.address)
+ return nil
+ }
+}
+
+func ensureUIPortAvailable(ctx context.Context, bindIP string, port int) error {
+ dialer := &net.Dialer{Timeout: readyDialTimeout}
+ address := net.JoinHostPort(dialHost(bindIP), strconv.Itoa(port))
+
+ conn, err := dialer.DialContext(ctx, "tcp", address)
+ if err == nil {
+ _ = conn.Close()
+ return fmt.Errorf(
+ "temporal ui port %d is already in use on %s; adjust configuration or stop the conflicting service",
+ port,
+ bindIP,
+ )
+ }
+ if !isConnRefused(err) {
+ return fmt.Errorf("verify temporal ui port %d on %s: %w", port, bindIP, err)
+ }
+ return nil
+}
+
+func waitForHTTPReady(ctx context.Context, address string, runErrCh <-chan error) error {
+ dialer := &net.Dialer{Timeout: readyDialTimeout}
+ ticker := time.NewTicker(readyPollInterval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case err, ok := <-runErrCh:
+ if ok && err != nil {
+ return err
+ }
+ if !ok {
+ return fmt.Errorf("ui server stopped before accepting connections")
+ }
+ case <-ticker.C:
+ conn, err := dialer.DialContext(ctx, "tcp", address)
+ if err == nil {
+ _ = conn.Close()
+ return nil
+ }
+ if !isConnRefused(err) && err != nil {
+ return fmt.Errorf("dial temporal ui address %s: %w", address, err)
+ }
+ }
+ }
+}
diff --git a/engine/worker/embedded/ui_test.go b/engine/worker/embedded/ui_test.go
new file mode 100644
index 00000000..c629ac6d
--- /dev/null
+++ b/engine/worker/embedded/ui_test.go
@@ -0,0 +1,124 @@
+package embedded
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "strconv"
+ "testing"
+ "time"
+
+ "github.com/compozy/compozy/pkg/logger"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestNewUIServer(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{
+ BindIP: "127.0.0.1",
+ FrontendPort: 7600,
+ Namespace: "default",
+ EnableUI: true,
+ UIPort: randomUIPort(t),
+ }
+
+ ui := newUIServer(cfg)
+ require.NotNil(t, ui)
+ assert.Equal(t, cfg, ui.config)
+ assert.Equal(t, cfg.EnableUI, ui.config.EnableUI)
+}
+
+func TestUIServerLifecycle(t *testing.T) {
+ t.Parallel()
+
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ cfg := &Config{
+ BindIP: "127.0.0.1",
+ FrontendPort: 7700,
+ Namespace: "default",
+ EnableUI: true,
+ UIPort: randomUIPort(t),
+ }
+
+ ui := newUIServer(cfg)
+ require.NotNil(t, ui)
+
+ startCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+ require.NoError(t, ui.Start(startCtx))
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+ui.address+"/health", http.NoBody)
+ require.NoError(t, err)
+ client := &http.Client{Timeout: time.Second}
+ resp, err := client.Do(req)
+ require.NoError(t, err)
+ require.NoError(t, resp.Body.Close())
+ assert.Equal(t, http.StatusOK, resp.StatusCode)
+
+ stopCtx, stopCancel := context.WithTimeout(ctx, 5*time.Second)
+ defer stopCancel()
+ require.NoError(t, ui.Stop(stopCtx))
+
+ dialer := &net.Dialer{Timeout: 200 * time.Millisecond}
+ _, err = dialer.DialContext(ctx, "tcp", ui.address)
+ require.Error(t, err)
+}
+
+func TestUIServerDisabled(t *testing.T) {
+ t.Parallel()
+
+ cfg := &Config{
+ EnableUI: false,
+ UIPort: 0,
+ }
+
+ assert.Nil(t, newUIServer(cfg))
+}
+
+func TestUIServerPortConflict(t *testing.T) {
+ t.Parallel()
+
+ ctx := logger.ContextWithLogger(t.Context(), logger.NewForTests())
+ port := randomUIPort(t)
+ listener := reserveUIPort(t, port)
+ defer func() { require.NoError(t, listener.Close()) }()
+
+ cfg := &Config{
+ BindIP: "127.0.0.1",
+ FrontendPort: 7800,
+ Namespace: "default",
+ EnableUI: true,
+ UIPort: port,
+ }
+
+ ui := newUIServer(cfg)
+ require.NotNil(t, ui)
+
+ startCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+ err := ui.Start(startCtx)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), strconv.Itoa(port))
+}
+
+func randomUIPort(t *testing.T) int {
+ t.Helper()
+
+ var lc net.ListenConfig
+ listener, err := lc.Listen(t.Context(), "tcp", "127.0.0.1:0")
+ require.NoError(t, err)
+ addr := listener.Addr().(*net.TCPAddr)
+ require.NoError(t, listener.Close())
+ return addr.Port
+}
+
+func reserveUIPort(t *testing.T, port int) net.Listener {
+ t.Helper()
+ var lc net.ListenConfig
+ addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
+ listener, err := lc.Listen(t.Context(), "tcp", addr)
+ require.NoError(t, err)
+ return listener
+}
diff --git a/examples/standalone/.env.example b/examples/standalone/.env.example
new file mode 100644
index 00000000..4f9cf6a8
--- /dev/null
+++ b/examples/standalone/.env.example
@@ -0,0 +1,2 @@
+# Replace with your model provider API key
+OPENAI_API_KEY=sk-your-openai-key
diff --git a/examples/standalone/README.md b/examples/standalone/README.md
new file mode 100644
index 00000000..d4787d81
--- /dev/null
+++ b/examples/standalone/README.md
@@ -0,0 +1,55 @@
+# Temporal Standalone Basic Example
+
+## Purpose
+
+Demonstrates the quickest path to running Compozy with the embedded Temporal server. Everything runs locally, including the Web UI, using the default in-memory configuration.
+
+## Key Concepts
+
+- Start Temporal in standalone mode without Docker or external services
+- Default ports (7233-7236) for the Temporal services and 8233 for the UI
+- In-memory persistence for fast restarts during development
+- Simple workflow execution and inspection through the UI
+
+## Prerequisites
+
+- Go 1.25.2 or newer installed
+- Node.js 20+ if you plan to run additional tooling
+- An API key for the model configured in `compozy.yaml` (see `.env.example`)
+
+## Quick Start
+
+```bash
+cd examples/temporal-standalone/basic
+cp .env.example .env
+compozy start
+```
+
+## Trigger the Workflow
+
+```bash
+compozy workflow trigger hello --input='{"name": "Temporal developer"}'
+```
+
+## Inspect in the UI
+
+1. Open in your browser
+2. Locate the `hello` workflow run in the Workflows list
+3. Expand the history to view task execution details
+
+## Expected Output
+
+- CLI shows `Embedded Temporal server started successfully` logs
+- Workflow result includes a greeting that echoes the provided name
+- Web UI shows the workflow in the `Completed` state with a single task
+
+## Troubleshooting
+
+- `address already in use`: another process is using port 7233 or 8233. Stop the other process or change the ports in `compozy.yaml`.
+- `missing API key`: ensure `.env` contains a valid key for the configured provider. Run `compozy config diagnostics` to confirm environment variables are detected.
+- Workflow stuck in `Running`: use the UI to inspect the history and confirm the agent completed. Retry after resolving any model issues.
+
+## What's Next
+
+- Read the standalone architecture overview: `../../../docs/content/docs/architecture/embedded-temporal.mdx`
+- Explore other configurations in this directory for persistence, custom ports, and debugging techniques
diff --git a/examples/standalone/api.http b/examples/standalone/api.http
new file mode 100644
index 00000000..d824c9c7
--- /dev/null
+++ b/examples/standalone/api.http
@@ -0,0 +1,20 @@
+### Temporal Standalone Basic Example API
+@baseUrl = http://localhost:5001/api/v0
+@workflowId = hello
+
+### Execute hello workflow
+# @name executeWorkflow
+POST {{baseUrl}}/workflows/{{workflowId}}/executions
+Content-Type: application/json
+Accept: application/json
+
+{
+ "input": {
+ "name": "Temporal developer"
+ }
+}
+
+### Get exec details
+@execId = {{executeWorkflow.response.body.data.exec_id}}
+GET {{baseUrl}}/executions/workflows/{{execId}}
+Accept: application/json
diff --git a/examples/standalone/compozy.yaml b/examples/standalone/compozy.yaml
new file mode 100644
index 00000000..c67337f4
--- /dev/null
+++ b/examples/standalone/compozy.yaml
@@ -0,0 +1,29 @@
+name: temporal-standalone-basic
+version: 0.1.0
+description: Minimal standalone Temporal configuration with in-memory persistence and Web UI enabled.
+
+temporal:
+ mode: standalone
+ host_port: 127.0.0.1:7233
+ namespace: default
+ task_queue: basic-example
+ standalone:
+ database_file: ":memory:"
+ # For persistency using SQLlite
+ # database_file: ./data/temporal.db
+ frontend_port: 7233
+ bind_ip: 127.0.0.1
+ namespace: default
+ cluster_name: compozy-standalone
+ enable_ui: true
+ ui_port: 8233
+ log_level: info
+
+models:
+ - provider: openai
+ model: gpt-4o-mini
+ api_key: "{{ .env.OPENAI_API_KEY }}"
+ default: true
+
+workflows:
+ - source: ./workflow.yaml
diff --git a/examples/standalone/workflow.yaml b/examples/standalone/workflow.yaml
new file mode 100644
index 00000000..5132fa74
--- /dev/null
+++ b/examples/standalone/workflow.yaml
@@ -0,0 +1,25 @@
+id: hello
+version: 0.1.0
+description: Greets the provided name to verify Temporal standalone mode is running.
+
+schemas:
+ - id: hello_input
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name to mention in the greeting
+ required:
+ - name
+
+config:
+ input: hello_input
+
+tasks:
+ - id: compose_greeting
+ type: basic
+ prompt: |-
+ You are a friendly assistant verifying that Temporal standalone mode works.
+ Respond with a short greeting that mentions {{ .workflow.input.name }} and
+ confirms the workflow executed in standalone mode.
+ final: true
diff --git a/examples/temporal-standalone/debugging/tools/index.ts b/examples/temporal-standalone/debugging/tools/index.ts
new file mode 100644
index 00000000..212b93d7
--- /dev/null
+++ b/examples/temporal-standalone/debugging/tools/index.ts
@@ -0,0 +1,19 @@
+type ValidationInput = {
+ value: number;
+};
+
+type ValidationOutput = {
+ value: number;
+};
+
+export default {
+ validate_input({ input }: { input: ValidationInput }): ValidationOutput {
+ if (!Number.isFinite(input.value)) {
+ throw new Error("value must be a finite number");
+ }
+ if (input.value <= 0) {
+ throw new Error(`value must be positive; received ${input.value}`);
+ }
+ return { value: input.value };
+ },
+};
diff --git a/examples/temporal-standalone/integration-testing/tests/integration_test.go b/examples/temporal-standalone/integration-testing/tests/integration_test.go
new file mode 100644
index 00000000..fdddd760
--- /dev/null
+++ b/examples/temporal-standalone/integration-testing/tests/integration_test.go
@@ -0,0 +1,78 @@
+//go:build integration
+
+package tests
+
+import (
+ "context"
+ "fmt"
+ "math/rand"
+ "net"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/engine/worker/embedded"
+ helpers "github.com/compozy/compozy/test/helpers"
+)
+
+func TestStandaloneServerLifecycle(t *testing.T) {
+ t.Parallel()
+
+ ctx := helpers.NewTestContext(t)
+ basePort := findOpenPortRange(t)
+
+ cfg := &embedded.Config{
+ DatabaseFile: ":memory:",
+ FrontendPort: basePort,
+ BindIP: "127.0.0.1",
+ Namespace: fmt.Sprintf("integration-%d", time.Now().UnixNano()),
+ ClusterName: "integration-testing",
+ EnableUI: false,
+ LogLevel: "warn",
+ StartTimeout: 20 * time.Second,
+ }
+
+ srv, err := embedded.NewServer(ctx, cfg)
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ require.NoError(t, srv.Stop(ctx))
+ })
+
+ startCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
+ defer cancel()
+ require.NoError(t, srv.Start(startCtx))
+ require.NotEmpty(t, srv.FrontendAddress())
+}
+
+// findOpenPortRange locates a contiguous 4-port window for Temporal standalone services.
+// The embedded server requires four consecutive ports (one per service) for CI/integration tests.
+func findOpenPortRange(t *testing.T) int {
+ t.Helper()
+ rng := rand.New(rand.NewSource(time.Now().UnixNano()))
+ for attempt := 0; attempt < 20; attempt++ {
+ base := 20000 + rng.Intn(20000)
+ listeners := make([]net.Listener, 0, 4)
+ success := true
+ for offset := 0; offset < 4; offset++ {
+ ln, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", base+offset))
+ if err != nil {
+ success = false
+ for _, existing := range listeners {
+ _ = existing.Close()
+ }
+ break
+ }
+ listeners = append(listeners, ln)
+ }
+ if success {
+ for _, ln := range listeners {
+ _ = ln.Close()
+ }
+ return base
+ }
+ }
+ t.Fatalf("unable to allocate contiguous port range for Temporal standalone services")
+ return 0
+}
diff --git a/go.mod b/go.mod
index e1e3c99a..2d77fcf9 100644
--- a/go.mod
+++ b/go.mod
@@ -70,11 +70,12 @@ require (
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.1
github.com/swaggo/swag v1.16.6
+ github.com/temporalio/ui-server/v2 v2.41.0
github.com/testcontainers/testcontainers-go v0.39.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0
github.com/tidwall/gjson v1.18.0
github.com/tidwall/pretty v1.2.1
- github.com/tmc/langchaingo v0.1.14
+ github.com/tmc/langchaingo v0.1.13
github.com/ulule/limiter/v3 v3.11.2
github.com/xhit/go-str2duration/v2 v2.1.0
go.opentelemetry.io/otel v1.38.0
@@ -83,8 +84,9 @@ require (
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
- go.temporal.io/api v1.54.0
+ go.temporal.io/api v1.53.0
go.temporal.io/sdk v1.37.0
+ go.temporal.io/server v1.29.0
golang.org/x/crypto v0.43.0
golang.org/x/net v0.46.0
golang.org/x/sync v0.17.0
@@ -106,20 +108,32 @@ require (
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
+ cloud.google.com/go/monitoring v1.24.2 // indirect
+ cloud.google.com/go/storage v1.53.0 // indirect
cloud.google.com/go/vertexai v0.12.0 // indirect
+ filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
+ github.com/apache/thrift v0.21.0 // indirect
+ github.com/aws/aws-sdk-go v1.55.6 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
github.com/aws/smithy-go v1.22.3 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
+ github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c // indirect
+ github.com/cactus/go-statsd-client/v5 v5.1.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -129,13 +143,16 @@ require (
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cohere-ai/cohere-go/v2 v2.14.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
+ github.com/coreos/go-oidc/v3 v3.11.0 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
@@ -144,11 +161,15 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
@@ -160,24 +181,35 @@ require (
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
+ github.com/gocql/gocql v1.7.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/mock v1.7.0-rc.1 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5 // indirect
github.com/google/flatbuffers v24.3.25+incompatible // indirect
github.com/google/generative-ai-go v0.20.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
+ github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
+ github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/huandu/xstrings v1.5.0 // indirect
+ github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/jolestar/go-commons-pool/v2 v2.1.2 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -186,14 +218,17 @@ require (
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
+ github.com/labstack/echo/v4 v4.13.4 // indirect
+ github.com/labstack/gommon v0.4.2 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mailru/easyjson v0.9.0 // indirect
- github.com/mattn/go-colorable v0.1.13 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
@@ -214,6 +249,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nexus-rpc/sdk-go v0.3.0 // indirect
github.com/nlpodyssey/cybertron v0.2.1 // indirect
github.com/nlpodyssey/gopickle v0.3.0 // indirect
@@ -221,52 +257,76 @@ require (
github.com/nlpodyssey/spago v1.1.0 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
+ github.com/olivere/elastic/v7 v7.0.32 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
+ github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/otlptranslator v0.0.2 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
- github.com/quic-go/quic-go v0.54.0 // indirect
+ github.com/quic-go/quic-go v0.54.1 // indirect
+ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/sashabaranov/go-openai v1.40.1 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sony/gobreaker v1.0.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
+ github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
+ github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 // indirect
+ github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb // indirect
+ github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/twmb/murmur3 v1.1.8 // indirect
+ github.com/uber-common/bark v1.3.0 // indirect
+ github.com/uber-go/tally/v4 v4.1.17 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
+ github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ github.com/zeebo/errs v1.4.0 // indirect
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 // indirect
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 // indirect
gitlab.com/golang-commonmark/markdown v0.0.0-20211110145824-bf3e522c626a // indirect
gitlab.com/golang-commonmark/mdurl v0.0.0-20191124015652-932350d1cb84 // indirect
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ go.uber.org/dig v1.18.0 // indirect
+ go.uber.org/fx v1.23.0 // indirect
go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.28.0 // indirect
@@ -277,7 +337,13 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.9 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/validator.v2 v2.0.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+ modernc.org/libc v1.66.3 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+ modernc.org/sqlite v1.38.2 // indirect
)
replace github.com/compozy/compozy/test/fixtures => ./test/fixtures
diff --git a/go.sum b/go.sum
index bd21dc74..fe761646 100644
--- a/go.sum
+++ b/go.sum
@@ -15,19 +15,40 @@ cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeO
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
+cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
+cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
+cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
+cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
+cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
+cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
+cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
+cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8=
cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
+github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -44,12 +65,18 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4=
github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
+github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
+github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE=
+github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
+github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
@@ -60,11 +87,22 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/benbjohnson/clock v0.0.0-20160125162948-a620c1cc9866/go.mod h1:UMqtWQTnOe4byzwe7Zhwh8f8s+36uszN51sJrSIZlTE=
+github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
+github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
+github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b h1:AP/Y7sqYicnjGDfD5VcY4CIfh1hRXBUavxrvELjTiOE=
+github.com/bmizerany/perks v0.0.0-20141205001514-d9a9656a3a4b/go.mod h1:ac9efd0D1fsDb3EJvhqgXRbFx7bs2wqZ10HQPeU8U/Q=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
@@ -76,6 +114,12 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cactus/go-statsd-client/statsd v0.0.0-20191106001114-12b4e2b38748/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
+github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c h1:HIGF0r/56+7fuIZw2V4isE22MK6xpxWx7BbV8dJ290w=
+github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
+github.com/cactus/go-statsd-client/v4 v4.0.0/go.mod h1:m73kwJp6TN0Ja9P6ycdZhWM1MlfxY/95WZ//IptPQ+Y=
+github.com/cactus/go-statsd-client/v5 v5.1.0 h1:sbbdfIl9PgisjEoXzvXI1lwUKWElngsjJKaZeC021P4=
+github.com/cactus/go-statsd-client/v5 v5.1.0/go.mod h1:COEvJ1E+/E2L4q6QE5CkjWPi4eeDw9maJBMIuMPBZbY=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
@@ -135,18 +179,23 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI=
+github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
+github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b/go.mod h1:v9FBN7gdVTpiD/+LZ7Po0UKvROyT87uLVxTHVky/dlQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
+github.com/dgryski/go-farm v0.0.0-20140601200337-fc41e106ee0e/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@@ -166,10 +215,15 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
+github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
+github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
@@ -181,6 +235,7 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -199,6 +254,11 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
+github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs=
+github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
+github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
@@ -233,6 +293,9 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
@@ -243,13 +306,19 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
+github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -262,6 +331,11 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5 h1:8QWUW69MXlNdZXnDnD9vEQ1BL8/mm1FTiSesKKHYivk=
+github.com/gomarkdown/markdown v0.0.0-20240729212818-a2a9c4f76ef5/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
@@ -274,21 +348,30 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
+github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.14.2 h1:eBLnkZ9635krYIPD+ag1USrOAI0Nr0QYF3+/3GqO0k0=
github.com/googleapis/gax-go/v2 v2.14.2/go.mod h1:ON64QhlJkhVtSqp4v1uaK92VyZ2gmvDQsweuyLV+8+w=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
@@ -299,10 +382,14 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4z
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
+github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
+github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
+github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
@@ -315,12 +402,18 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM=
@@ -330,6 +423,7 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/kaptinlin/go-i18n v0.2.0 h1:8iwjAERQbCVF78c3HxC4MxUDxDRFvQVQlMDvlsO43hU=
@@ -354,12 +448,21 @@ github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ
github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w=
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
+github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
@@ -380,8 +483,9 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA=
github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -390,6 +494,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
@@ -440,6 +546,7 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA=
github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nlpodyssey/cybertron v0.2.1 h1:zBvzmjP6Teq3u8yiHuLoUPxan6ZDRq/32GpV6Ep8X08=
github.com/nlpodyssey/cybertron v0.2.1/go.mod h1:Vg9PeB8EkOTAgSKQ68B3hhKUGmB6Vs734dBdCyE4SVM=
github.com/nlpodyssey/gopickle v0.3.0 h1:BLUE5gxFLyyNOPzlXxt6GoHEMMxD0qhsE4p0CIQyoLw=
@@ -452,6 +559,8 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
+github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
+github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
github.com/ollama/ollama v0.12.4 h1:VfqVk8qSxREJar0z0EQAU2/h9qi/cqAMIKzo+nT+faI=
github.com/ollama/ollama v0.12.4/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms=
github.com/onsi/ginkgo/v2 v2.25.3 h1:Ty8+Yi/ayDAGtk4XxmmfUy4GabvM+MegeB4cDLRi6nw=
@@ -464,12 +573,17 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pashagolub/pgxmock/v4 v4.9.0 h1:itlO8nrVRnzkdMBXLs8pWUyyB2PC3Gku0WGIj/gGl7I=
github.com/pashagolub/pgxmock/v4 v4.9.0/go.mod h1:9L57pC193h2aKRHVyiiE817avasIPZnPwPlw3JczWvM=
+github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
+github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
@@ -489,6 +603,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a h1:AA9vgIBDjMHPC2McaGPojgV2dcI78ZC0TLNhYCXEKH8=
+github.com/prashantv/protectmem v0.0.0-20171002184600-e20412882b3a/go.mod h1:lzZQ3Noex5pfAy7mkAeCjcBDteYU85uWWnJ/y6gKU8k=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
@@ -508,10 +624,13 @@ github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7D
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
-github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
-github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
+github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
+github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
+github.com/rcrowley/go-metrics v0.0.0-20141108142129-dee209f2455f/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
+github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -523,6 +642,7 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/romdo/go-debounce v0.1.0 h1:x5cEkVabfsX+9Orkc6v6sktozqGYRqjvCmAg8P1CgWI=
@@ -534,6 +654,7 @@ github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3V
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/samuel/go-thrift v0.0.0-20190219015601-e8b6b52668fe/go.mod h1:Vrkh1pnjV9Bl8c3P9zH0/D4NlOHWP5d4/hF4YTULaec=
github.com/sashabaranov/go-openai v1.40.1 h1:bJ08Iwct5mHBVkuvG6FEcb9MDTfsXdTYPGjYLRdeTEU=
github.com/sashabaranov/go-openai v1.40.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
@@ -544,10 +665,14 @@ github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dI
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sirupsen/logrus v1.0.2-0.20170726183946-abee6f9b0679/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slok/goresilience v0.2.0 h1:dagdIiWlhTm7BK/r/LRKz+zvw0SCNk+nHf7obdsbzxQ=
github.com/slok/goresilience v0.2.0/go.mod h1:L6IqqHlxWGTrTyq8WwF8kUY8kOIESZAMWr1xkV0zdZA=
+github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
+github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
@@ -555,6 +680,8 @@ github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=
+github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -565,6 +692,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -578,6 +707,15 @@ github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
+github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7 h1:lEebX/hZss+TSH3EBwhztnBavJVj7pWGJOH8UgKHS0w=
+github.com/temporalio/ringpop-go v0.0.0-20250130211428-b97329e994f7/go.mod h1:RE+CHmY+kOZQk47AQaVzwrGmxpflnLgTd6EOK0853j4=
+github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb h1:YzHH/U/dN7vMP+glybzcXRTczTrgfdRisNTzAj7La04=
+github.com/temporalio/sqlparser v0.0.0-20231115171017-f4060bcfa6cb/go.mod h1:143qKdh3G45IgV9p+gbAwp3ikRDI8mxsijFiXDfuxsw=
+github.com/temporalio/tchannel-go v1.22.1-0.20220818200552-1be8d8cffa5b/go.mod h1:c+V9Z/ZgkzAdyGvHrvC5AsXgN+M9Qwey04cBdKYzV7U=
+github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938 h1:sEJGhmDo+0FaPWM6f0v8Tjia0H5pR6/Baj6+kS78B+M=
+github.com/temporalio/tchannel-go v1.22.1-0.20240528171429-1db37fdea938/go.mod h1:ezRQRwu9KQXy8Wuuv1aaFFxoCNz5CeNbVOOkh3xctbY=
+github.com/temporalio/ui-server/v2 v2.41.0 h1:m9F2jnFJy/dWxjk9d2oS+825r78EX4gUlipoQ1xNO6Y=
+github.com/temporalio/ui-server/v2 v2.41.0/go.mod h1:ofEKGV5/NaPbjdmEQRcUDFE6nZPprOemNjLJYLF9IX4=
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM=
@@ -595,12 +733,24 @@ github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8O
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
-github.com/tmc/langchaingo v0.1.14 h1:o1qWBPigAIuFvrG6cjTFo0cZPFEZ47ZqpOYMjM15yZc=
-github.com/tmc/langchaingo v0.1.14/go.mod h1:aKKYXYoqhIDEv7WKdpnnCLRaqXic69cX9MnDUk72378=
+github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
+github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
+github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
+github.com/uber-common/bark v1.0.0/go.mod h1:g0ZuPcD7XiExKHynr93Q742G/sbrdVQkghrqLGOoFuY=
+github.com/uber-common/bark v1.3.0 h1:DkuZCBaQS9LWuNAPrCO6yQVANckIX3QI0QwLemUnzCo=
+github.com/uber-common/bark v1.3.0/go.mod h1:5fDe/YcIVP55XhFF9hUihX2lDsDcpFrTZEAwAVwtPDw=
+github.com/uber-go/tally v3.3.15+incompatible/go.mod h1:YDTIBxdXyOU/sCWilKB4bgyufu1cEi0jdVnRdxvjnmU=
+github.com/uber-go/tally/v4 v4.1.17 h1:C+U4BKtVDXTszuzU+WH8JVQvRVnaVKxzZrROFyDrvS8=
+github.com/uber-go/tally/v4 v4.1.17/go.mod h1:ZdpiHRGSa3z4NIAc1VlEH4SiknR885fOIF08xmS0gaU=
+github.com/uber/jaeger-client-go v2.22.1+incompatible h1:NHcubEkVbahf9t3p75TOCR83gdUHXjRJvjoBh1yACsM=
+github.com/uber/jaeger-client-go v2.22.1+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
+github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
@@ -611,6 +761,10 @@ github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOAp
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
@@ -639,6 +793,8 @@ github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
+github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181 h1:K+bMSIx9A7mLES1rtG+qKduLIXq40DAzYHtb0XuCukA=
gitlab.com/golang-commonmark/html v0.0.0-20191124015941-a22733972181/go.mod h1:dzYhVIwWCtzPAa4QP98wfB9+mzt33MSmM8wsKiMi2ow=
gitlab.com/golang-commonmark/linkify v0.0.0-20191026162114-a0c2df6c8f82 h1:oYrL81N608MLZhma3ruL8qTM4xcpYECGut8KSxRY59g=
@@ -653,18 +809,28 @@ gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638 h1:uPZaMiz6Sz0PZs3IZJW
gitlab.com/opennota/wd v0.0.0-20180912061657-c5d65f63c638/go.mod h1:EGRJaqe2eO9XGmFtQCvV3Lm9NLico3UhFwUpCG/+mVU=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/collector/pdata v1.34.0 h1:2vwYftckXe7pWxI9mfSo+tw3wqdGNrYpMbDx/5q6rw8=
+go.opentelemetry.io/collector/pdata v1.34.0/go.mod h1:StPHMFkhLBellRWrULq0DNjv4znCDJZP6La4UuC+JHI=
+go.opentelemetry.io/contrib/detectors/gcp v1.36.0 h1:F7q2tNlCaHY9nMKHR6XH9/qkp8FktLnIcy6jJNyOCQw=
+go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0 h1:ajl4QczuJVA2TU9W9AGw++86Xga/RKt//16z/yxPgdk=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.34.0/go.mod h1:Vn3/rlOJ3ntf/Q3zAI0V5lDnTbHGaUsNUeF6nZmm7pA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo=
go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
@@ -677,39 +843,70 @@ go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOV
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
-go.temporal.io/api v1.54.0 h1:/sy8rYZEykgmXRjeiv1PkFHLXIus5n6FqGhRtCl7Pc0=
-go.temporal.io/api v1.54.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
+go.temporal.io/api v1.53.0 h1:6vAFpXaC584AIELa6pONV56MTpkm4Ha7gPWL2acNAjo=
+go.temporal.io/api v1.53.0/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/sdk v1.37.0 h1:RbwCkUQuqY4rfCzdrDZF9lgT7QWG/pHlxfZFq0NPpDQ=
go.temporal.io/sdk v1.37.0/go.mod h1:tOy6vGonfAjrpCl6Bbw/8slTgQMiqvoyegRv2ZHPm5M=
+go.temporal.io/server v1.29.0 h1:BGBCvI7vcPokCjuDsfitLx2eS+8ow+yQ4frLJZcn2nQ=
+go.temporal.io/server v1.29.0/go.mod h1:pc0n6DRcN06V4WNhaxdxE3KaZIS3KSDNKdca6uu6RuU=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
+go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
+go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
+go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
+go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -724,7 +921,10 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -737,15 +937,20 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -758,13 +963,17 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -773,28 +982,41 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
+gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/api v0.235.0 h1:C3MkpQSRxS1Jy6AkzTGKKrpSCOd2WOGrezZ+icKSkKo=
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -824,11 +1046,21 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
+gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
+gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
@@ -839,15 +1071,35 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
+modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
+modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
+modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
+modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
+modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 0378b015..63992b55 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -399,6 +399,13 @@ type DatabaseConfig struct {
// namespace: default
// task_queue: compozy-tasks
type TemporalConfig struct {
+ // Mode controls how the application connects to Temporal.
+ //
+ // Values:
+ // - "remote": Connect to an external Temporal cluster (default)
+ // - "standalone": Launch embedded Temporal server for local development and tests
+ Mode string `koanf:"mode" env:"TEMPORAL_MODE" json:"mode" yaml:"mode" mapstructure:"mode" validate:"required,oneof=remote standalone"`
+
// HostPort specifies the Temporal server endpoint.
//
// Format: `host:port`
@@ -424,7 +431,46 @@ type TemporalConfig struct {
// Default: "compozy-tasks"
TaskQueue string `koanf:"task_queue" env:"TEMPORAL_TASK_QUEUE" json:"task_queue" yaml:"task_queue" mapstructure:"task_queue"`
- // Embedded Temporal not supported; dev server flag removed.
+ // Standalone configures embedded Temporal when Mode is set to "standalone".
+ Standalone StandaloneConfig `koanf:"standalone" env_prefix:"TEMPORAL_STANDALONE" json:"standalone" yaml:"standalone" mapstructure:"standalone"`
+}
+
+// StandaloneConfig configures the embedded Temporal server.
+//
+// These options mirror the embedded server configuration so users can manage development
+// and test environments without touching production settings.
+type StandaloneConfig struct {
+ // DatabaseFile specifies the SQLite database location.
+ //
+ // Use ":memory:" for ephemeral storage or provide a file path for persistence.
+ DatabaseFile string `koanf:"database_file" env:"TEMPORAL_STANDALONE_DATABASE_FILE" json:"database_file" yaml:"database_file" mapstructure:"database_file"`
+
+ // FrontendPort sets the gRPC port for the Temporal frontend service.
+ FrontendPort int `koanf:"frontend_port" env:"TEMPORAL_STANDALONE_FRONTEND_PORT" json:"frontend_port" yaml:"frontend_port" mapstructure:"frontend_port"`
+
+ // BindIP determines the IP address Temporal services bind to.
+ BindIP string `koanf:"bind_ip" env:"TEMPORAL_STANDALONE_BIND_IP" json:"bind_ip" yaml:"bind_ip" mapstructure:"bind_ip"`
+
+ // Namespace specifies the default namespace created on startup.
+ Namespace string `koanf:"namespace" env:"TEMPORAL_STANDALONE_NAMESPACE" json:"namespace" yaml:"namespace" mapstructure:"namespace"`
+
+ // ClusterName customizes the Temporal cluster name for standalone mode.
+ ClusterName string `koanf:"cluster_name" env:"TEMPORAL_STANDALONE_CLUSTER_NAME" json:"cluster_name" yaml:"cluster_name" mapstructure:"cluster_name"`
+
+ // EnableUI toggles the Temporal Web UI server.
+ EnableUI bool `koanf:"enable_ui" env:"TEMPORAL_STANDALONE_ENABLE_UI" json:"enable_ui" yaml:"enable_ui" mapstructure:"enable_ui"`
+
+ // RequireUI enforces UI availability; startup fails when UI cannot be launched.
+ RequireUI bool `koanf:"require_ui" env:"TEMPORAL_STANDALONE_REQUIRE_UI" json:"require_ui" yaml:"require_ui" mapstructure:"require_ui"`
+
+ // UIPort sets the HTTP port for the Temporal Web UI.
+ UIPort int `koanf:"ui_port" env:"TEMPORAL_STANDALONE_UI_PORT" json:"ui_port" yaml:"ui_port" mapstructure:"ui_port"`
+
+ // LogLevel controls Temporal server logging verbosity.
+ LogLevel string `koanf:"log_level" env:"TEMPORAL_STANDALONE_LOG_LEVEL" json:"log_level" yaml:"log_level" mapstructure:"log_level"`
+
+ // StartTimeout defines the maximum startup wait duration.
+ StartTimeout time.Duration `koanf:"start_timeout" env:"TEMPORAL_STANDALONE_START_TIMEOUT" json:"start_timeout" yaml:"start_timeout" mapstructure:"start_timeout"`
}
// RuntimeConfig contains runtime behavior configuration.
@@ -2137,9 +2183,22 @@ func buildDatabaseConfig(registry *definition.Registry) DatabaseConfig {
func buildTemporalConfig(registry *definition.Registry) TemporalConfig {
return TemporalConfig{
+ Mode: getString(registry, "temporal.mode"),
HostPort: getString(registry, "temporal.host_port"),
Namespace: getString(registry, "temporal.namespace"),
TaskQueue: getString(registry, "temporal.task_queue"),
+ Standalone: StandaloneConfig{
+ DatabaseFile: getString(registry, "temporal.standalone.database_file"),
+ FrontendPort: getInt(registry, "temporal.standalone.frontend_port"),
+ BindIP: getString(registry, "temporal.standalone.bind_ip"),
+ Namespace: getString(registry, "temporal.standalone.namespace"),
+ ClusterName: getString(registry, "temporal.standalone.cluster_name"),
+ EnableUI: getBool(registry, "temporal.standalone.enable_ui"),
+ RequireUI: getBool(registry, "temporal.standalone.require_ui"),
+ UIPort: getInt(registry, "temporal.standalone.ui_port"),
+ LogLevel: getString(registry, "temporal.standalone.log_level"),
+ StartTimeout: getDuration(registry, "temporal.standalone.start_timeout"),
+ },
}
}
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 81816906..4e2204d0 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -46,9 +46,19 @@ func TestConfig_Default(t *testing.T) {
assert.Equal(t, "disable", cfg.Database.SSLMode)
// Temporal defaults
+ assert.Equal(t, "remote", cfg.Temporal.Mode)
assert.Equal(t, "localhost:7233", cfg.Temporal.HostPort)
assert.Equal(t, "default", cfg.Temporal.Namespace)
assert.Equal(t, "compozy-tasks", cfg.Temporal.TaskQueue)
+ assert.Equal(t, ":memory:", cfg.Temporal.Standalone.DatabaseFile)
+ assert.Equal(t, 7233, cfg.Temporal.Standalone.FrontendPort)
+ assert.Equal(t, "127.0.0.1", cfg.Temporal.Standalone.BindIP)
+ assert.Equal(t, cfg.Temporal.Namespace, cfg.Temporal.Standalone.Namespace)
+ assert.Equal(t, "compozy-standalone", cfg.Temporal.Standalone.ClusterName)
+ assert.True(t, cfg.Temporal.Standalone.EnableUI)
+ assert.Equal(t, 8233, cfg.Temporal.Standalone.UIPort)
+ assert.Equal(t, "warn", cfg.Temporal.Standalone.LogLevel)
+ assert.Equal(t, 30*time.Second, cfg.Temporal.Standalone.StartTimeout)
// Runtime defaults
assert.Equal(t, "development", cfg.Runtime.Environment)
@@ -110,6 +120,47 @@ func TestConfig_Default(t *testing.T) {
})
}
+func TestTemporalStandaloneMode(t *testing.T) {
+ t.Run("Should apply standalone defaults when mode set to standalone", func(t *testing.T) {
+ ctx := t.Context()
+ manager := NewManager(ctx, NewService())
+ overrides := map[string]any{
+ "temporal-mode": "standalone",
+ }
+ cfg, err := manager.Load(ctx, NewDefaultProvider(), NewCLIProvider(overrides))
+ require.NoError(t, err)
+ require.NotNil(t, cfg)
+ assert.Equal(t, "standalone", cfg.Temporal.Mode)
+ assert.Equal(t, ":memory:", cfg.Temporal.Standalone.DatabaseFile)
+ assert.Equal(t, 7233, cfg.Temporal.Standalone.FrontendPort)
+ assert.Equal(t, "127.0.0.1", cfg.Temporal.Standalone.BindIP)
+ assert.Equal(t, cfg.Temporal.Namespace, cfg.Temporal.Standalone.Namespace)
+ assert.Equal(t, "compozy-standalone", cfg.Temporal.Standalone.ClusterName)
+ assert.True(t, cfg.Temporal.Standalone.EnableUI)
+ assert.Equal(t, 8233, cfg.Temporal.Standalone.UIPort)
+ assert.Equal(t, "warn", cfg.Temporal.Standalone.LogLevel)
+ assert.Equal(t, 30*time.Second, cfg.Temporal.Standalone.StartTimeout)
+ assert.Equal(t, "localhost:7233", cfg.Temporal.HostPort)
+ assert.Equal(t, "default", cfg.Temporal.Namespace)
+ _ = manager.Close(ctx)
+ })
+
+ t.Run("Should allow host port override in standalone mode", func(t *testing.T) {
+ ctx := t.Context()
+ manager := NewManager(ctx, NewService())
+ overrides := map[string]any{
+ "temporal-mode": "standalone",
+ "temporal-host": "0.0.0.0:9000",
+ }
+ cfg, err := manager.Load(ctx, NewDefaultProvider(), NewCLIProvider(overrides))
+ require.NoError(t, err)
+ require.NotNil(t, cfg)
+ assert.Equal(t, "standalone", cfg.Temporal.Mode)
+ assert.Equal(t, "0.0.0.0:9000", cfg.Temporal.HostPort)
+ _ = manager.Close(ctx)
+ })
+}
+
func TestLLMConfig_StructuredOutputRetryPrecedence(t *testing.T) {
t.Setenv("LLM_STRUCTURED_OUTPUT_RETRIES", "3")
ctx := t.Context()
@@ -147,6 +198,119 @@ func TestDefaultNativeToolsConfig(t *testing.T) {
}
func TestConfig_Validation(t *testing.T) {
+ t.Run("Should validate temporal mode", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ mode string
+ wantErr bool
+ }{
+ {name: "remote", mode: "remote", wantErr: false},
+ {name: "standalone", mode: "standalone", wantErr: false},
+ {name: "invalid", mode: "invalid", wantErr: true},
+ {name: "empty", mode: "", wantErr: true},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := Default()
+ cfg.Temporal.Mode = tc.mode
+ svc := NewService()
+ err := svc.Validate(cfg)
+ if tc.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ })
+ }
+ })
+
+ t.Run("Should validate standalone configuration when mode standalone", func(t *testing.T) {
+ testCases := []struct {
+ name string
+ mutate func(*TemporalConfig)
+ wantErr string
+ }{
+ {
+ name: "missing database file",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.DatabaseFile = ""
+ },
+ wantErr: "database_file",
+ },
+ {
+ name: "frontend port out of range",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.FrontendPort = 0
+ },
+ wantErr: "frontend_port",
+ },
+ {
+ name: "bind ip invalid",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.BindIP = "not-an-ip"
+ },
+ wantErr: "bind_ip",
+ },
+ {
+ name: "missing namespace",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.Namespace = ""
+ },
+ wantErr: "namespace",
+ },
+ {
+ name: "missing cluster name",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.ClusterName = ""
+ },
+ wantErr: "cluster_name",
+ },
+ {
+ name: "ui port invalid when ui enabled",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.UIPort = 0
+ },
+ wantErr: "ui_port",
+ },
+ {
+ name: "invalid log level",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.LogLevel = "trace"
+ },
+ wantErr: "log_level",
+ },
+ {
+ name: "non positive start timeout",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.StartTimeout = 0
+ },
+ wantErr: "start_timeout",
+ },
+ {
+ name: "ui disabled allows zero port",
+ mutate: func(cfg *TemporalConfig) {
+ cfg.Standalone.EnableUI = false
+ cfg.Standalone.UIPort = 0
+ },
+ wantErr: "",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := Default()
+ cfg.Temporal.Mode = "standalone"
+ tc.mutate(&cfg.Temporal)
+ svc := NewService()
+ err := svc.Validate(cfg)
+ if tc.wantErr == "" {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), tc.wantErr)
+ }
+ })
+ }
+ })
t.Run("Should validate server port range", func(t *testing.T) {
tests := []struct {
name string
diff --git a/pkg/config/definition/schema.go b/pkg/config/definition/schema.go
index 17e36522..ad90761b 100644
--- a/pkg/config/definition/schema.go
+++ b/pkg/config/definition/schema.go
@@ -928,6 +928,20 @@ func registerDatabasePoolTimeoutFields(registry *Registry) {
)
}
func registerTemporalFields(registry *Registry) {
+ registerTemporalCoreFields(registry)
+ registerTemporalStandaloneServerFields(registry)
+ registerTemporalStandaloneRuntimeFields(registry)
+}
+
+func registerTemporalCoreFields(registry *Registry) {
+ registry.Register(&FieldDef{
+ Path: "temporal.mode",
+ Default: "remote",
+ CLIFlag: "temporal-mode",
+ EnvVar: "TEMPORAL_MODE",
+ Type: reflect.TypeOf(""),
+ Help: "Temporal connection mode: remote (production) or standalone (development/testing)",
+ })
registry.Register(&FieldDef{
Path: "temporal.host_port",
Default: "localhost:7233",
@@ -954,6 +968,85 @@ func registerTemporalFields(registry *Registry) {
})
}
+func registerTemporalStandaloneServerFields(registry *Registry) {
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.database_file",
+ Default: ":memory:",
+ CLIFlag: "temporal-standalone-database",
+ EnvVar: "TEMPORAL_STANDALONE_DATABASE_FILE",
+ Type: reflect.TypeOf(""),
+ Help: "SQLite database path for standalone Temporal server (:memory: for in-memory)",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.frontend_port",
+ Default: 7233,
+ CLIFlag: "temporal-standalone-frontend-port",
+ EnvVar: "TEMPORAL_STANDALONE_FRONTEND_PORT",
+ Type: reflect.TypeOf(0),
+ Help: "Frontend gRPC port for standalone Temporal server",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.bind_ip",
+ Default: "127.0.0.1",
+ EnvVar: "TEMPORAL_STANDALONE_BIND_IP",
+ Type: reflect.TypeOf(""),
+ Help: "IP address to bind standalone Temporal services",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.namespace",
+ Default: "default",
+ EnvVar: "TEMPORAL_STANDALONE_NAMESPACE",
+ Type: reflect.TypeOf(""),
+ Help: "Default namespace created in standalone Temporal server",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.cluster_name",
+ Default: "compozy-standalone",
+ EnvVar: "TEMPORAL_STANDALONE_CLUSTER_NAME",
+ Type: reflect.TypeOf(""),
+ Help: "Cluster name for standalone Temporal server",
+ })
+}
+
+func registerTemporalStandaloneRuntimeFields(registry *Registry) {
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.enable_ui",
+ Default: true,
+ EnvVar: "TEMPORAL_STANDALONE_ENABLE_UI",
+ Type: reflect.TypeOf(true),
+ Help: "Enable Temporal Web UI in standalone mode",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.require_ui",
+ Default: false,
+ EnvVar: "TEMPORAL_STANDALONE_REQUIRE_UI",
+ Type: reflect.TypeOf(true),
+ Help: "Fail startup when Temporal Web UI cannot be launched in standalone mode",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.ui_port",
+ Default: 8233,
+ CLIFlag: "temporal-standalone-ui-port",
+ EnvVar: "TEMPORAL_STANDALONE_UI_PORT",
+ Type: reflect.TypeOf(0),
+ Help: "HTTP port for Temporal Web UI in standalone mode",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.log_level",
+ Default: "warn",
+ EnvVar: "TEMPORAL_STANDALONE_LOG_LEVEL",
+ Type: reflect.TypeOf(""),
+ Help: "Temporal server log level (debug, info, warn, error) in standalone mode",
+ })
+ registry.Register(&FieldDef{
+ Path: "temporal.standalone.start_timeout",
+ Default: 30 * time.Second,
+ EnvVar: "TEMPORAL_STANDALONE_START_TIMEOUT",
+ Type: durationType,
+ Help: "Maximum duration to wait for standalone Temporal server startup",
+ })
+}
+
func registerRuntimeFields(registry *Registry) {
registerRuntimeCoreFields(registry)
registerRuntimeDispatcherFields(registry)
diff --git a/pkg/config/loader.go b/pkg/config/loader.go
index 07c12b1b..42c339ee 100644
--- a/pkg/config/loader.go
+++ b/pkg/config/loader.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"maps"
+ "net"
"reflect"
"strconv"
"strings"
@@ -18,6 +19,12 @@ import (
"github.com/knadh/koanf/v2"
)
+const (
+ maxTCPPort = 65535
+ temporalServiceSpan = 3 // Temporal reserves FrontendPort through FrontendPort+3
+ temporalModeStandalone = "standalone"
+)
+
// loader implements the Service interface for configuration management.
type loader struct {
koanf *koanf.Koanf
@@ -373,8 +380,107 @@ func validateDatabase(cfg *Config) error {
}
func validateTemporal(cfg *Config) error {
- if cfg.Temporal.HostPort == "" {
- return fmt.Errorf("temporal host_port is required")
+ mode := strings.TrimSpace(cfg.Temporal.Mode)
+ if mode == "" {
+ return fmt.Errorf("temporal.mode is required")
+ }
+ switch mode {
+ case "remote":
+ if cfg.Temporal.HostPort == "" {
+ return fmt.Errorf("temporal.host_port is required in remote mode")
+ }
+ return nil
+ case temporalModeStandalone:
+ return validateStandaloneTemporalConfig(cfg)
+ default:
+ return fmt.Errorf("temporal.mode must be one of [remote standalone], got %q", mode)
+ }
+}
+
+func validateStandaloneTemporalConfig(cfg *Config) error {
+ standalone := &cfg.Temporal.Standalone
+ if err := validateStandaloneDatabase(standalone); err != nil {
+ return err
+ }
+ if err := validateStandalonePorts(standalone); err != nil {
+ return err
+ }
+ if err := validateStandaloneNetwork(standalone); err != nil {
+ return err
+ }
+ if err := validateStandaloneMetadata(standalone); err != nil {
+ return err
+ }
+ if err := validateStandaloneLogLevel(standalone); err != nil {
+ return err
+ }
+ return validateStandaloneStartTimeout(standalone)
+}
+
+func validateStandaloneDatabase(standalone *StandaloneConfig) error {
+ if standalone.DatabaseFile == "" {
+ return fmt.Errorf("temporal.standalone.database_file is required when mode=standalone")
+ }
+ return nil
+}
+
+func validateStandalonePorts(standalone *StandaloneConfig) error {
+ if standalone.FrontendPort < 1 || standalone.FrontendPort > maxTCPPort {
+ return fmt.Errorf("temporal.standalone.frontend_port must be between 1 and %d", maxTCPPort)
+ }
+ if standalone.FrontendPort+temporalServiceSpan > maxTCPPort {
+ return fmt.Errorf("temporal.standalone.frontend_port reserves out-of-range service port")
+ }
+ if standalone.EnableUI {
+ if standalone.UIPort < 1 || standalone.UIPort > maxTCPPort {
+ return fmt.Errorf("temporal.standalone.ui_port must be between 1 and %d when enable_ui is true", maxTCPPort)
+ }
+ start := standalone.FrontendPort
+ end := standalone.FrontendPort + temporalServiceSpan
+ if standalone.UIPort >= start && standalone.UIPort <= end {
+ return fmt.Errorf("temporal.standalone.ui_port must not collide with service ports [%d-%d]", start, end)
+ }
+ } else if standalone.UIPort != 0 && (standalone.UIPort < 1 || standalone.UIPort > maxTCPPort) {
+ return fmt.Errorf("temporal.standalone.ui_port must be between 1 and %d when set", maxTCPPort)
+ }
+ return nil
+}
+
+func validateStandaloneNetwork(standalone *StandaloneConfig) error {
+ if standalone.BindIP == "" {
+ return fmt.Errorf("temporal.standalone.bind_ip is required when mode=standalone")
+ }
+ if net.ParseIP(standalone.BindIP) == nil {
+ return fmt.Errorf("temporal.standalone.bind_ip must be a valid IP address")
+ }
+ return nil
+}
+
+func validateStandaloneMetadata(standalone *StandaloneConfig) error {
+ if standalone.Namespace == "" {
+ return fmt.Errorf("temporal.standalone.namespace is required when mode=standalone")
+ }
+ if standalone.ClusterName == "" {
+ return fmt.Errorf("temporal.standalone.cluster_name is required when mode=standalone")
+ }
+ return nil
+}
+
+func validateStandaloneLogLevel(standalone *StandaloneConfig) error {
+ switch standalone.LogLevel {
+ case "debug", "info", "warn", "error":
+ return nil
+ default:
+ return fmt.Errorf(
+ "temporal.standalone.log_level must be one of [debug info warn error], got %q",
+ standalone.LogLevel,
+ )
+ }
+}
+
+func validateStandaloneStartTimeout(standalone *StandaloneConfig) error {
+ if standalone.StartTimeout <= 0 {
+ return fmt.Errorf("temporal.standalone.start_timeout must be positive")
}
return nil
}
@@ -485,14 +591,14 @@ func validateNativeToolTimeouts(cfg *Config) error {
return nil
}
-// validateTCPPort validates that a string represents a valid TCP port number (1-65535)
+// validateTCPPort validates that a string represents a valid TCP port number (1-maxTCPPort)
func validateTCPPort(portStr, fieldName string) error {
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("%s must be a valid integer, got: %s", fieldName, portStr)
}
- if port < 1 || port > 65535 {
- return fmt.Errorf("%s must be between 1 and 65535, got: %d", fieldName, port)
+ if port < 1 || port > maxTCPPort {
+ return fmt.Errorf("%s must be between 1 and %d, got: %d", fieldName, maxTCPPort, port)
}
return nil
}
diff --git a/pkg/config/provider.go b/pkg/config/provider.go
index ce6e279d..6d12aa6d 100644
--- a/pkg/config/provider.go
+++ b/pkg/config/provider.go
@@ -363,9 +363,21 @@ func createDatabaseDefaults(defaultConfig *Config) map[string]any {
// createTemporalDefaults creates temporal configuration defaults
func createTemporalDefaults(defaultConfig *Config) map[string]any {
return map[string]any{
+ "mode": defaultConfig.Temporal.Mode,
"host_port": defaultConfig.Temporal.HostPort,
"namespace": defaultConfig.Temporal.Namespace,
"task_queue": defaultConfig.Temporal.TaskQueue,
+ "standalone": map[string]any{
+ "database_file": defaultConfig.Temporal.Standalone.DatabaseFile,
+ "frontend_port": defaultConfig.Temporal.Standalone.FrontendPort,
+ "bind_ip": defaultConfig.Temporal.Standalone.BindIP,
+ "namespace": defaultConfig.Temporal.Standalone.Namespace,
+ "cluster_name": defaultConfig.Temporal.Standalone.ClusterName,
+ "enable_ui": defaultConfig.Temporal.Standalone.EnableUI,
+ "ui_port": defaultConfig.Temporal.Standalone.UIPort,
+ "log_level": defaultConfig.Temporal.Standalone.LogLevel,
+ "start_timeout": defaultConfig.Temporal.Standalone.StartTimeout.String(),
+ },
}
}
diff --git a/schemas/config.json b/schemas/config.json
index c83b2331..0690be36 100644
--- a/schemas/config.json
+++ b/schemas/config.json
@@ -1147,6 +1147,14 @@
"additionalProperties": false,
"description": "TemporalConfig contains Temporal workflow engine configuration.",
"properties": {
+ "mode": {
+ "description": "Mode controls how Compozy connects to Temporal.\n\nValues:\n - \"remote\": connect to an external Temporal deployment (default)\n - \"standalone\": launch the embedded Temporal server for local development\n\nStandalone mode is intended for development and testing only.",
+ "enum": [
+ "remote",
+ "standalone"
+ ],
+ "type": "string"
+ },
"host_port": {
"description": "HostPort specifies the Temporal server endpoint.\n\nFormat: `host:port`\nDefault: \"localhost:7233\"",
"type": "string"
@@ -1158,6 +1166,53 @@
"task_queue": {
"description": "TaskQueue identifies the queue for workflow tasks.\n\nWorkers poll this queue for tasks to execute.\nUse different queues for:\n - Workflow type separation\n - Priority-based routing\n - Resource isolation\nDefault: \"compozy-tasks\"",
"type": "string"
+ },
+ "standalone": {
+ "$ref": "#/$defs/TemporalStandaloneConfig",
+ "description": "Standalone configures the embedded Temporal server when `mode` is \"standalone\"."
+ }
+ },
+ "type": "object"
+ },
+ "TemporalStandaloneConfig": {
+ "additionalProperties": false,
+ "description": "TemporalStandaloneConfig configures the embedded Temporal server used in standalone mode.",
+ "properties": {
+ "bind_ip": {
+ "description": "BindIP determines the IP address the Temporal services listen on.\n\nDefault: \"127.0.0.1\"",
+ "type": "string"
+ },
+ "cluster_name": {
+ "description": "ClusterName customizes the Temporal cluster identifier for standalone deployments.\n\nDefault: \"compozy-standalone\"",
+ "type": "string"
+ },
+ "database_file": {
+ "description": "DatabaseFile specifies the SQLite database location for Temporal persistence.\n\nUse `\":memory:\"` for ephemeral storage or provide a file path for persistence across restarts.\nDefault: `\":memory:\"`",
+ "type": "string"
+ },
+ "enable_ui": {
+ "description": "EnableUI toggles the Temporal Web UI server for local debugging.\n\nDefault: `true`",
+ "type": "boolean"
+ },
+ "frontend_port": {
+ "description": "FrontendPort sets the Temporal frontend gRPC port.\n\nDefault: `7233`",
+ "type": "integer"
+ },
+ "log_level": {
+ "description": "LogLevel controls Temporal server logging verbosity.\n\nValues: \"debug\", \"info\", \"warn\", \"error\"\nDefault: \"warn\"",
+ "type": "string"
+ },
+ "namespace": {
+ "description": "Namespace specifies the default namespace created when the embedded server starts.\n\nDefault: \"default\"",
+ "type": "string"
+ },
+ "start_timeout": {
+ "description": "StartTimeout defines the maximum duration to wait for the embedded Temporal server to start.\n\nDefault: `30s`",
+ "type": "integer"
+ },
+ "ui_port": {
+ "description": "UIPort sets the HTTP port for the Temporal Web UI.\n\nDefault: `8233`",
+ "type": "integer"
}
},
"type": "object"
diff --git a/scripts/markdown/check.go b/scripts/markdown/check.go
index 18695ce6..17c25d46 100644
--- a/scripts/markdown/check.go
+++ b/scripts/markdown/check.go
@@ -26,18 +26,28 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
+ "github.com/sethvargo/go-retry"
"github.com/spf13/cobra"
"github.com/tidwall/pretty"
)
const (
- unknownFileName = "unknown"
- ideCodex = "codex"
- ideClaude = "claude"
- ideDroid = "droid"
- defaultCodexModel = "gpt-5-codex"
- defaultClaudeModel = "sonnet[1m]"
- thinkPromptMedium = "Think hard through problems carefully before acting. Balance speed with thoroughness."
+ unknownFileName = "unknown"
+ ideCodex = "codex"
+ ideClaude = "claude"
+ ideDroid = "droid"
+ defaultCodexModel = "gpt-5-codex"
+ defaultClaudeModel = "claude-sonnet-4-5-20250929"
+ defaultActivityTimeout = 10 * time.Minute
+ exitCodeTimeout = -2
+ exitCodeCanceled = -1
+ activityCheckInterval = 5 * time.Second
+ processTerminationGracePeriod = 5 * time.Second
+ gracefulShutdownTimeout = 30 * time.Second
+ uiMessageDrainDelay = 80 * time.Millisecond
+ uiTickInterval = 120 * time.Millisecond
+ thinkPromptMedium = "Think hard through problems carefully before acting. " +
+ "Balance speed with thoroughness."
thinkPromptLow = "Think concisely and act quickly. Prefer direct solutions."
thinkPromptHighDescription = "Ultrathink deeply and comprehensively before taking action. " +
"Consider edge cases, alternatives, and long-term implications. Show your reasoning process."
@@ -73,18 +83,21 @@ const (
// (default: medium, options: low/medium/high).
type cliArgs struct {
- pr string
- issuesDir string
- dryRun bool
- concurrent int
- batchSize int
- ide string
- model string
- grouped bool
- tailLines int
- reasoningEffort string
- mode string
- includeCompleted bool
+ pr string
+ issuesDir string
+ dryRun bool
+ concurrent int
+ batchSize int
+ ide string
+ model string
+ grouped bool
+ tailLines int
+ reasoningEffort string
+ mode string
+ includeCompleted bool
+ timeout time.Duration
+ maxRetries int
+ retryBackoffMultiplier float64
}
type issueEntry struct {
@@ -151,19 +164,22 @@ Behavior:
}
var (
- pr string
- issuesDir string
- dryRun bool
- concurrent int
- batchSize int
- ide string
- model string
- grouped bool
- tailLines int
- reasoningEffort string
- useForm bool
- mode string
- includeCompleted bool
+ pr string
+ issuesDir string
+ dryRun bool
+ concurrent int
+ batchSize int
+ ide string
+ model string
+ grouped bool
+ tailLines int
+ reasoningEffort string
+ useForm bool
+ mode string
+ includeCompleted bool
+ timeout string
+ maxRetries int
+ retryBackoffMultiplier float64
)
var _ = buildZenMCPGuidance
@@ -197,6 +213,24 @@ func setupFlags() {
)
rootCmd.Flags().
BoolVar(&includeCompleted, "include-completed", false, "Include completed tasks (only applies to prd-tasks mode)")
+ rootCmd.Flags().StringVar(
+ &timeout,
+ "timeout",
+ "10m",
+ "Activity timeout duration (e.g., 5m, 30s). Job canceled if no output received within this period.",
+ )
+ rootCmd.Flags().IntVar(
+ &maxRetries,
+ "max-retries",
+ 3,
+ "Maximum number of retry attempts on timeout (0 = no retry, default: 3)",
+ )
+ rootCmd.Flags().Float64Var(
+ &retryBackoffMultiplier,
+ "retry-backoff-multiplier",
+ 2.0,
+ "Timeout multiplier for each retry attempt (default: 2.0 = 2x timeout on each retry)",
+ )
// Note: PR is usually required, but we handle this dynamically in runSolveIssues
}
@@ -225,6 +259,7 @@ type formInputs struct {
tailLines string
reasoningEffort string
mode string
+ timeout string
}
func newFormInputs() *formInputs {
@@ -242,6 +277,7 @@ func (fi *formInputs) register(builder *formBuilder) {
builder.addModelField(&fi.model)
builder.addTailLinesField(&fi.tailLines)
builder.addReasoningEffortField(&fi.reasoningEffort)
+ builder.addTimeoutField(&fi.timeout)
builder.addConfirmField(
"dry-run",
"Dry Run?",
@@ -270,6 +306,7 @@ func (fi *formInputs) apply(cmd *cobra.Command) {
applyStringInput(cmd, "reasoning-effort", fi.reasoningEffort, func(val string) {
reasoningEffort = val
})
+ applyStringInput(cmd, "timeout", fi.timeout, func(val string) { timeout = val })
}
type formBuilder struct {
@@ -290,7 +327,10 @@ func (fb *formBuilder) addField(flag string, build func() huh.Field) {
if fb.cmd.Flags().Changed(flag) {
return
}
- fb.fields = append(fb.fields, build())
+ field := build()
+ if field != nil {
+ fb.fields = append(fb.fields, field)
+ }
}
func (fb *formBuilder) addModeField(target *string) {
@@ -308,14 +348,24 @@ func (fb *formBuilder) addModeField(target *string) {
func (fb *formBuilder) addPRField(target *string) {
fb.addField("pr", func() huh.Field {
+ title := "PR Number"
+ placeholder := "259"
+ description := "Required: Pull request number or identifier to process"
+ errorMsg := "PR number is required"
+ if mode == modePRDTasks {
+ title = "Task Identifier"
+ placeholder = "multi-repo"
+ description = "Required: Task name/identifier (e.g., 'multi-repo' for tasks/prd-multi-repo)"
+ errorMsg = "Task identifier is required"
+ }
return huh.NewInput().
- Title("PR Number").
- Placeholder("259").
- Description("Required: Pull request number or identifier to process").
+ Title(title).
+ Placeholder(placeholder).
+ Description(description).
Value(target).
Validate(func(str string) error {
if str == "" {
- return errors.New("PR number is required")
+ return errors.New(errorMsg)
}
return nil
})
@@ -324,10 +374,18 @@ func (fb *formBuilder) addPRField(target *string) {
func (fb *formBuilder) addOptionalPathField(flag string, target *string) {
fb.addField(flag, func() huh.Field {
+ title := "Issues Directory (optional)"
+ placeholder := "ai-docs/reviews-pr-/issues"
+ description := "Leave empty to auto-generate from PR number"
+ if mode == modePRDTasks {
+ title = "Tasks Directory (optional)"
+ placeholder = "tasks/prd-"
+ description = "Leave empty to auto-generate from task identifier"
+ }
return huh.NewInput().
- Title("Issues Directory (optional)").
- Placeholder("ai-docs/reviews-pr-/issues").
- Description("Leave empty to auto-generate from PR number").
+ Title(title).
+ Placeholder(placeholder).
+ Description(description).
Value(target)
})
}
@@ -416,6 +474,26 @@ func (fb *formBuilder) addReasoningEffortField(target *string) {
})
}
+func (fb *formBuilder) addTimeoutField(target *string) {
+ fb.addField("timeout", func() huh.Field {
+ return huh.NewInput().
+ Title("Activity Timeout").
+ Placeholder("10m").
+ Description("Cancel job if no output received within this period (e.g., 5m, 30s)").
+ Value(target).
+ Validate(func(str string) error {
+ if str == "" {
+ return nil
+ }
+ _, err := time.ParseDuration(str)
+ if err != nil {
+ return errors.New("invalid duration format (e.g., 5m, 30s, 1h)")
+ }
+ return nil
+ })
+ })
+}
+
func (fb *formBuilder) addConfirmField(flag, title, description string, target *bool) {
fb.addField(flag, func() huh.Field {
return huh.NewConfirm().
@@ -521,19 +599,28 @@ func ensurePRProvided() error {
}
func buildCLIArgs() *cliArgs {
+ timeoutDuration := defaultActivityTimeout
+ if timeout != "" {
+ if parsed, err := time.ParseDuration(timeout); err == nil {
+ timeoutDuration = parsed
+ }
+ }
return &cliArgs{
- pr: pr,
- issuesDir: issuesDir,
- dryRun: dryRun,
- concurrent: concurrent,
- batchSize: batchSize,
- ide: ide,
- model: model,
- grouped: grouped,
- tailLines: tailLines,
- reasoningEffort: reasoningEffort,
- mode: mode,
- includeCompleted: includeCompleted,
+ pr: pr,
+ issuesDir: issuesDir,
+ dryRun: dryRun,
+ concurrent: concurrent,
+ batchSize: batchSize,
+ ide: ide,
+ model: model,
+ grouped: grouped,
+ tailLines: tailLines,
+ reasoningEffort: reasoningEffort,
+ mode: mode,
+ includeCompleted: includeCompleted,
+ timeout: timeoutDuration,
+ maxRetries: maxRetries,
+ retryBackoffMultiplier: retryBackoffMultiplier,
}
}
@@ -561,6 +648,12 @@ func (c *cliArgs) validate() error {
c.batchSize,
)
}
+ if c.maxRetries < 0 {
+ return fmt.Errorf("max-retries cannot be negative (got %d)", c.maxRetries)
+ }
+ if c.retryBackoffMultiplier <= 0 {
+ return fmt.Errorf("retry-backoff-multiplier must be positive (got %.2f)", c.retryBackoffMultiplier)
+ }
return nil
}
@@ -688,7 +781,11 @@ func resolveInputs(args *cliArgs) (string, string, string, error) {
}
}
if issuesDir == "" {
- issuesDir = fmt.Sprintf("ai-docs/reviews-pr-%s/issues", pr)
+ if args.mode == modePRDTasks {
+ issuesDir = fmt.Sprintf("tasks/prd-%s", pr)
+ } else {
+ issuesDir = fmt.Sprintf("ai-docs/reviews-pr-%s/issues", pr)
+ }
}
resolvedIssuesDir, err := filepath.Abs(issuesDir)
if err != nil {
@@ -910,7 +1007,7 @@ func newJobExecutionContext(ctx context.Context, jobs []job, args *cliArgs) (*jo
func (j *jobExecutionContext) cleanup() {
if j.uiProg != nil {
close(j.uiCh)
- time.Sleep(80 * time.Millisecond)
+ time.Sleep(uiMessageDrainDelay)
j.uiProg.Quit()
}
}
@@ -982,7 +1079,7 @@ func (j *jobExecutionContext) reportAggregateUsage() {
}
func (j *jobExecutionContext) awaitShutdownAfterCancel(done <-chan struct{}) (int32, []failInfo, int, error) {
- shutdownTimeout := 30 * time.Second
+ shutdownTimeout := gracefulShutdownTimeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer shutdownCancel()
select {
@@ -1060,7 +1157,7 @@ func runOneJob(
useUI := uiCh != nil
if ctx.Err() != nil {
if useUI {
- uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: -1}
+ uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: exitCodeCanceled}
}
return
}
@@ -1071,11 +1168,146 @@ func runOneJob(
}
return
}
- cmd, outF, errF := setupCommandExecution(ctx, args, j, cwd, useUI, uiCh, index, aggregateUsage, aggregateMu)
+ executeJobWithRetry(
+ ctx, args, j, cwd, useUI, uiCh, index,
+ failed, failuresMu, failures, aggregateUsage, aggregateMu,
+ )
+}
+
+func executeJobWithRetry(
+ ctx context.Context,
+ args *cliArgs,
+ j *job,
+ cwd string,
+ useUI bool,
+ uiCh chan uiMsg,
+ index int,
+ failed *int32,
+ failuresMu *sync.Mutex,
+ failures *[]failInfo,
+ aggregateUsage *TokenUsage,
+ aggregateMu *sync.Mutex,
+) {
+ currentTimeout := args.timeout
+ attempt := 0
+ maxRetries := uint64(0)
+ if args.maxRetries > 0 {
+ // #nosec G115 - maxRetries is validated to be non-negative and reasonable
+ maxRetries = uint64(args.maxRetries)
+ }
+ backoff := retry.WithMaxRetries(maxRetries, retry.NewConstant(1*time.Millisecond))
+ err := retry.Do(ctx, backoff, func(retryCtx context.Context) error {
+ attempt++
+ currentTimeout = calculateRetryTimeout(
+ currentTimeout,
+ attempt,
+ args.retryBackoffMultiplier,
+ args.maxRetries,
+ index,
+ j,
+ useUI,
+ )
+ return executeJobAttempt(
+ retryCtx, args, j, cwd, useUI, uiCh, index, currentTimeout,
+ failed, failuresMu, failures, aggregateUsage, aggregateMu, attempt,
+ )
+ })
+ logRetryCompletion(err, attempt, index, j, useUI)
+}
+
+func calculateRetryTimeout(
+ currentTimeout time.Duration,
+ attempt int,
+ multiplier float64,
+ maxRetries int,
+ index int,
+ j *job,
+ useUI bool,
+) time.Duration {
+ if attempt > 1 {
+ currentTimeout = time.Duration(float64(currentTimeout) * multiplier)
+ if !useUI {
+ fmt.Fprintf(
+ os.Stderr,
+ "\nπ Retry attempt %d/%d for job %d (%s) with timeout %v\n",
+ attempt-1,
+ maxRetries,
+ index+1,
+ strings.Join(j.codeFiles, ", "),
+ currentTimeout,
+ )
+ }
+ }
+ return currentTimeout
+}
+
+func executeJobAttempt(
+ ctx context.Context,
+ args *cliArgs,
+ j *job,
+ cwd string,
+ useUI bool,
+ uiCh chan uiMsg,
+ index int,
+ currentTimeout time.Duration,
+ failed *int32,
+ failuresMu *sync.Mutex,
+ failures *[]failInfo,
+ aggregateUsage *TokenUsage,
+ aggregateMu *sync.Mutex,
+ attempt int,
+) error {
+ argsWithTimeout := *args
+ argsWithTimeout.timeout = currentTimeout
+ if useUI && attempt > 1 {
+ uiCh <- jobStartedMsg{Index: index}
+ }
+ success, exitCode := executeJobWithTimeoutAndResult(
+ ctx, &argsWithTimeout, j, cwd, useUI, uiCh,
+ index, failed, failuresMu, failures, aggregateUsage, aggregateMu,
+ )
+ if !success && exitCode == exitCodeTimeout {
+ return retry.RetryableError(fmt.Errorf("timeout"))
+ }
+ return nil
+}
+
+func logRetryCompletion(err error, attempt int, index int, j *job, useUI bool) {
+ if err != nil && attempt > 1 && !useUI {
+ fmt.Fprintf(
+ os.Stderr,
+ "\nβ Job %d (%s) failed after %d retry attempts\n",
+ index+1,
+ strings.Join(j.codeFiles, ", "),
+ attempt-1,
+ )
+ }
+}
+
+func executeJobWithTimeoutAndResult(
+ ctx context.Context,
+ args *cliArgs,
+ j *job,
+ cwd string,
+ useUI bool,
+ uiCh chan uiMsg,
+ index int,
+ failed *int32,
+ failuresMu *sync.Mutex,
+ failures *[]failInfo,
+ aggregateUsage *TokenUsage,
+ aggregateMu *sync.Mutex,
+) (bool, int) {
+ cmd, outF, errF, monitor := setupCommandExecution(
+ ctx, args, j, cwd, useUI, uiCh, index, aggregateUsage, aggregateMu,
+ )
if cmd == nil {
- return
+ return false, -1
}
- executeCommandAndHandleResult(ctx, cmd, outF, errF, j, index, useUI, uiCh, failed, failuresMu, failures)
+ return executeCommandAndHandleResultWithStatus(
+ ctx, args.timeout, monitor, cmd, outF, errF, j,
+ index, useUI, uiCh, failed, failuresMu, failures,
+ )
}
func notifyJobStart(useUI bool, uiCh chan uiMsg, index int, j *job, ide string, model string, reasoningEffort string) {
@@ -1277,17 +1509,18 @@ func setupCommandIO(
ideType string,
aggregateUsage *TokenUsage,
aggregateMu *sync.Mutex,
-) (*os.File, *os.File, error) {
+) (*os.File, *os.File, *activityMonitor, error) {
configureCommandEnvironment(cmd, cwd, j.prompt)
outF, err := createLogFile(j.outLog, "out")
if err != nil {
- return nil, nil, fmt.Errorf("create out log: %w", err)
+ return nil, nil, nil, fmt.Errorf("create out log: %w", err)
}
errF, err := createLogFile(j.errLog, "err")
if err != nil {
outF.Close()
- return nil, nil, fmt.Errorf("create err log: %w", err)
+ return nil, nil, nil, fmt.Errorf("create err log: %w", err)
}
+ monitor := newActivityMonitor()
outTap, errTap := buildCommandTaps(
outF,
errF,
@@ -1298,10 +1531,11 @@ func setupCommandIO(
ideType,
aggregateUsage,
aggregateMu,
+ monitor,
)
cmd.Stdout = outTap
cmd.Stderr = errTap
- return outF, errF, nil
+ return outF, errF, monitor, nil
}
// configureCommandEnvironment applies working directory, stdin, and color env vars.
@@ -1325,13 +1559,14 @@ func buildCommandTaps(
ideType string,
aggregateUsage *TokenUsage,
aggregateMu *sync.Mutex,
+ monitor *activityMonitor,
) (io.Writer, io.Writer) {
outRing := newLineRing(tailLines)
errRing := newLineRing(tailLines)
if useUI {
- return buildUITaps(outF, errF, outRing, errRing, uiCh, index, ideType, aggregateUsage, aggregateMu)
+ return buildUITaps(outF, errF, outRing, errRing, uiCh, index, ideType, aggregateUsage, aggregateMu, monitor)
}
- return buildCLITaps(outF, errF, ideType, aggregateUsage, aggregateMu)
+ return buildCLITaps(outF, errF, ideType, aggregateUsage, aggregateMu, monitor)
}
// buildUITaps creates stdout/stderr writers when the interactive UI is enabled.
@@ -1343,8 +1578,9 @@ func buildUITaps(
ideType string,
aggregateUsage *TokenUsage,
aggregateMu *sync.Mutex,
+ monitor *activityMonitor,
) (io.Writer, io.Writer) {
- uiTap := newUILogTap(index, false, outRing, errRing, uiCh)
+ uiTap := newUILogTap(index, false, outRing, errRing, uiCh, monitor)
var outTap io.Writer
if ideType == ideClaude {
usageCallback := func(usage TokenUsage) {
@@ -1357,11 +1593,11 @@ func buildUITaps(
aggregateMu.Unlock()
}
}
- outTap = io.MultiWriter(outF, newJSONFormatterWithCallback(uiTap, usageCallback))
+ outTap = io.MultiWriter(outF, newJSONFormatterWithCallbackAndMonitor(uiTap, usageCallback, monitor))
} else {
outTap = io.MultiWriter(outF, uiTap)
}
- errTap := io.MultiWriter(errF, newUILogTap(index, true, outRing, errRing, uiCh))
+ errTap := io.MultiWriter(errF, newUILogTap(index, true, outRing, errRing, uiCh, monitor))
return outTap, errTap
}
@@ -1371,6 +1607,7 @@ func buildCLITaps(
ideType string,
aggregateUsage *TokenUsage,
aggregateMu *sync.Mutex,
+ monitor *activityMonitor,
) (io.Writer, io.Writer) {
if ideType == ideClaude {
usageCallback := func(usage TokenUsage) {
@@ -1382,7 +1619,7 @@ func buildCLITaps(
}
return io.MultiWriter(
outF,
- newJSONFormatterWithCallback(os.Stdout, usageCallback),
+ newJSONFormatterWithCallbackAndMonitor(os.Stdout, usageCallback, monitor),
), io.MultiWriter(
errF,
os.Stderr,
@@ -1401,12 +1638,12 @@ func setupCommandExecution(
index int,
aggregateUsage *TokenUsage,
aggregateMu *sync.Mutex,
-) (*exec.Cmd, *os.File, *os.File) {
+) (*exec.Cmd, *os.File, *os.File, *activityMonitor) {
cmd := createIDECommand(ctx, args)
if cmd == nil {
- return nil, nil, nil
+ return nil, nil, nil, nil
}
- outF, errF, err := setupCommandIO(
+ outF, errF, monitor, err := setupCommandIO(
cmd,
j,
cwd,
@@ -1420,21 +1657,23 @@ func setupCommandExecution(
)
if err != nil {
recordFailureWithContext(nil, j, nil, err, -1)
- return nil, nil, nil
+ return nil, nil, nil, nil
}
- return cmd, outF, errF
+ return cmd, outF, errF, monitor
}
func createLogFile(path, _ string) (*os.File, error) {
- file, err := os.Create(path)
+ file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return nil, err
}
return file, nil
}
-func executeCommandAndHandleResult(
+func executeCommandAndHandleResultWithStatus(
ctx context.Context,
+ timeout time.Duration,
+ monitor *activityMonitor,
cmd *exec.Cmd,
outF *os.File,
errF *os.File,
@@ -1445,7 +1684,7 @@ func executeCommandAndHandleResult(
failed *int32,
failuresMu *sync.Mutex,
failures *[]failInfo,
-) {
+) (bool, int) {
defer func() {
if outF != nil {
outF.Close()
@@ -1458,15 +1697,59 @@ func executeCommandAndHandleResult(
go func() {
cmdDone <- cmd.Run()
}()
+ activityTimeout := startActivityWatchdog(ctx, monitor, timeout, cmdDone)
+ type result struct {
+ success bool
+ exitCode int
+ }
+ resultCh := make(chan result, 1)
select {
case err := <-cmdDone:
- handleCommandCompletion(err, j, index, useUI, uiCh, failed, failuresMu, failures)
+ success, exitCode := handleCommandCompletionWithResult(
+ err, j, index, useUI, uiCh, failed, failuresMu, failures,
+ )
+ resultCh <- result{success, exitCode}
+ case <-activityTimeout:
+ handleActivityTimeout(ctx, cmd, cmdDone, j, index, useUI, uiCh, failed, failuresMu, failures, timeout)
+ resultCh <- result{false, exitCodeTimeout}
case <-ctx.Done():
handleCommandCancellation(ctx, cmd, cmdDone, j, index, useUI, uiCh, failed, failuresMu, failures)
+ resultCh <- result{false, exitCodeCanceled}
}
+ res := <-resultCh
+ return res.success, res.exitCode
}
-func handleCommandCompletion(
+func startActivityWatchdog(
+ ctx context.Context,
+ monitor *activityMonitor,
+ timeout time.Duration,
+ cmdDone <-chan error,
+) <-chan struct{} {
+ activityTimeout := make(chan struct{})
+ if monitor != nil && timeout > 0 {
+ go func() {
+ ticker := time.NewTicker(activityCheckInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ if monitor.timeSinceLastActivity() > timeout {
+ close(activityTimeout)
+ return
+ }
+ case <-cmdDone:
+ return
+ case <-ctx.Done():
+ return
+ }
+ }
+ }()
+ }
+ return activityTimeout
+}
+
+func handleCommandCompletionWithResult(
err error,
j *job,
index int,
@@ -1475,7 +1758,7 @@ func handleCommandCompletion(
failed *int32,
failuresMu *sync.Mutex,
failures *[]failInfo,
-) {
+) (bool, int) {
if err != nil {
ec := exitCodeOf(err)
atomic.AddInt32(failed, 1)
@@ -1488,11 +1771,12 @@ func handleCommandCompletion(
if useUI {
uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: ec}
}
- return
+ return false, ec
}
if useUI {
uiCh <- jobFinishedMsg{Index: index, Success: true, ExitCode: 0}
}
+ return true, 0
}
func handleCommandCancellation(
@@ -1522,7 +1806,7 @@ func handleCommandCancellation(
select {
case <-cmdDone:
fmt.Fprintf(os.Stderr, "Job %d terminated gracefully\n", index+1)
- case <-time.After(5 * time.Second):
+ case <-time.After(processTerminationGracePeriod):
// NOTE: Escalate to SIGKILL if the process ignores our grace period.
fmt.Fprintf(os.Stderr, "Job %d did not terminate gracefully, force killing...\n", index+1)
if err := cmd.Process.Kill(); err != nil {
@@ -1531,8 +1815,92 @@ func handleCommandCancellation(
}
}
if useUI {
- uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: -1}
+ uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: exitCodeCanceled}
+ }
+}
+
+func handleActivityTimeout(
+ _ context.Context,
+ cmd *exec.Cmd,
+ cmdDone <-chan error,
+ j *job,
+ index int,
+ useUI bool,
+ uiCh chan uiMsg,
+ failed *int32,
+ failuresMu *sync.Mutex,
+ failures *[]failInfo,
+ timeout time.Duration,
+) {
+ logTimeoutMessage(index, j, timeout, useUI)
+ atomic.AddInt32(failed, 1)
+ terminateTimedOutProcess(cmd, cmdDone, index, useUI)
+ recordTimeoutFailure(j, timeout, failuresMu, failures)
+ if useUI {
+ uiCh <- jobFinishedMsg{Index: index, Success: false, ExitCode: exitCodeTimeout}
+ }
+}
+
+func logTimeoutMessage(index int, j *job, timeout time.Duration, useUI bool) {
+ if !useUI {
+ fmt.Fprintf(
+ os.Stderr,
+ "\nJob %d (%s) timed out after %v of inactivity\n",
+ index+1,
+ strings.Join(j.codeFiles, ", "),
+ timeout,
+ )
+ }
+}
+
+func terminateTimedOutProcess(cmd *exec.Cmd, cmdDone <-chan error, index int, useUI bool) {
+ if cmd.Process == nil {
+ return
+ }
+ if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
+ if !useUI {
+ fmt.Fprintf(os.Stderr, "Failed to send SIGTERM to process: %v\n", err)
+ }
}
+ waitForProcessTermination(cmdDone, cmd, index, useUI)
+}
+
+func waitForProcessTermination(cmdDone <-chan error, cmd *exec.Cmd, index int, useUI bool) {
+ select {
+ case <-cmdDone:
+ if !useUI {
+ fmt.Fprintf(os.Stderr, "Job %d terminated gracefully after timeout\n", index+1)
+ }
+ case <-time.After(processTerminationGracePeriod):
+ forceKillProcess(cmd, index, useUI)
+ }
+}
+
+func forceKillProcess(cmd *exec.Cmd, index int, useUI bool) {
+ if !useUI {
+ fmt.Fprintf(os.Stderr, "Job %d did not terminate gracefully, force killing...\n", index+1)
+ }
+ if err := cmd.Process.Kill(); err != nil {
+ if !useUI {
+ fmt.Fprintf(os.Stderr, "Failed to kill process: %v\n", err)
+ }
+ }
+}
+
+func recordTimeoutFailure(j *job, timeout time.Duration, failuresMu *sync.Mutex, failures *[]failInfo) {
+ codeFileLabel := strings.Join(j.codeFiles, ", ")
+ timeoutErr := fmt.Errorf("activity timeout: no output received for %v", timeout)
+ recordFailure(
+ failuresMu,
+ failures,
+ failInfo{
+ codeFile: codeFileLabel,
+ exitCode: exitCodeTimeout,
+ outLog: j.outLog,
+ errLog: j.errLog,
+ err: timeoutErr,
+ },
+ )
}
func recordFailureWithContext(
@@ -1792,7 +2160,7 @@ func (m *uiModel) waitEvent() tea.Cmd {
}
func (m *uiModel) tick() tea.Cmd {
- return tea.Tick(120*time.Millisecond, func(time.Time) tea.Msg { return tickMsg{} })
+ return tea.Tick(uiTickInterval, func(time.Time) tea.Msg { return tickMsg{} })
}
func (m *uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -2548,7 +2916,7 @@ func readTaskEntries(tasksDir string, includeCompleted bool) ([]issueEntry, erro
if !f.Type().IsRegular() || !strings.HasSuffix(f.Name(), ".md") {
continue
}
- if strings.HasPrefix(f.Name(), "_") && !strings.HasPrefix(f.Name(), "_task_") {
+ if !reTaskFile.MatchString(f.Name()) {
continue
}
names = append(names, f.Name())
@@ -2569,7 +2937,7 @@ func readTaskEntries(tasksDir string, includeCompleted bool) ([]issueEntry, erro
name: name,
absPath: absPath,
content: content,
- codeFile: task.domain,
+ codeFile: strings.TrimSuffix(name, ".md"),
})
}
return entries, nil
@@ -2629,6 +2997,7 @@ func filterUnresolved(all []issueEntry) []issueEntry {
var (
reResolvedStatus = regexp.MustCompile(`(?mi)^\s*(status|state)\s*:\s*resolved\b`)
reResolvedTask = regexp.MustCompile(`(?mi)^\s*-\s*\[(x|X)\]\s*resolved\b`)
+ reTaskFile = regexp.MustCompile(`^_task_\d+\.md$`)
)
func isIssueResolved(content string) bool {
@@ -2835,8 +3204,19 @@ func buildCodeReviewPrompt(p buildBatchedIssuesParams) string {
func buildPRDTaskPrompt(task issueEntry) string {
taskData := parseTaskFile(task.content)
+ prdDir := filepath.Dir(task.absPath)
+ tasksFile := filepath.Join(prdDir, "_tasks.md")
+ header := fmt.Sprintf("# Implementation Task: %s\n\n", task.name)
+ contextSection := buildTaskContextSection(&taskData)
+ criticalSection := buildCriticalExecutionSection()
+ specSection := fmt.Sprintf("## Task Specification\n\n%s\n\n", task.content)
+ implSection := buildImplementationInstructionsSection(prdDir)
+ completionSection := buildCompletionCriteriaSection(task.absPath, tasksFile, task.name)
+ return header + contextSection + criticalSection + specSection + implSection + completionSection
+}
+
+func buildTaskContextSection(taskData *taskEntry) string {
var sb strings.Builder
- sb.WriteString(fmt.Sprintf("# Implementation Task: %s\n\n", task.name))
sb.WriteString("## Task Context\n\n")
sb.WriteString(fmt.Sprintf("- **Domain**: %s\n", taskData.domain))
sb.WriteString(fmt.Sprintf("- **Type**: %s\n", taskData.taskType))
@@ -2846,31 +3226,81 @@ func buildPRDTaskPrompt(task issueEntry) string {
sb.WriteString(fmt.Sprintf("- **Dependencies**: %s\n", strings.Join(taskData.dependencies, ", ")))
}
sb.WriteString("\n")
- sb.WriteString("## Task Specification\n\n")
- sb.WriteString(task.content)
- sb.WriteString("\n\n")
+ return sb.String()
+}
+
+func buildCriticalExecutionSection() string {
+ var sb strings.Builder
+ sb.WriteString("\n")
+ sb.WriteString("**EXECUTION MODE: ONE-SHOT DIRECT IMPLEMENTATION**\n\n")
+ sb.WriteString("You MUST complete this task in ONE continuous execution from beginning to end:\n\n")
+ sb.WriteString("- **NO ASKING**: Do not ask for clarification, confirmation, or approval\n")
+ sb.WriteString("- **NO PLANNING MODE**: Execute directly without presenting plans\n")
+ sb.WriteString("- **NO PARTIAL WORK**: Complete ALL requirements, subtasks, and deliverables\n")
+ sb.WriteString("- **FOLLOW ALL STANDARDS**: Adhere to ALL project rules in `.cursor/rules/`\n")
+ sb.WriteString("- **BEST PRACTICES ONLY**: No workarounds, hacks, or shortcuts\n")
+ sb.WriteString("- **PROPER SOLUTIONS**: Implement production-ready, maintainable code\n\n")
+ sb.WriteString("**VALIDATION REQUIREMENTS**:\n")
+ sb.WriteString("- All tests MUST pass (`make test`)\n")
+ sb.WriteString("- All linting MUST pass (`make lint`)\n")
+ sb.WriteString("- All type checking MUST pass (`make typecheck`)\n")
+ sb.WriteString("- All subtasks MUST be marked complete\n")
+ sb.WriteString("- Task status MUST be updated to 'completed'\n\n")
+ sb.WriteString("β οΈ **WORK WILL BE INVALIDATED** if:\n")
+ sb.WriteString("- Any requirement is incomplete\n")
+ sb.WriteString("- Tests/linting/typecheck fail\n")
+ sb.WriteString("- Project standards are violated\n")
+ sb.WriteString("- Workarounds are used instead of proper solutions\n")
+ sb.WriteString("- Task completion steps are skipped\n")
+ sb.WriteString("\n\n")
+ return sb.String()
+}
+
+func buildImplementationInstructionsSection(prdDir string) string {
+ var sb strings.Builder
sb.WriteString("## Implementation Instructions\n\n")
sb.WriteString("\n")
sb.WriteString("**MANDATORY READ BEFORE STARTING**:\n")
- sb.WriteString("- @.cursor/rules/go-coding-standards.mdc\n")
- sb.WriteString("- @.cursor/rules/architecture.mdc\n")
- sb.WriteString("- @.cursor/rules/test-standards.mdc\n")
+ sb.WriteString("- @.cursor/rules/critical-validation.mdc\n")
+ sb.WriteString(fmt.Sprintf("- All documents from this PRD directory: `%s`\n", prdDir))
+ sb.WriteString(" - Especially review `_techspec.md` and `_tasks.md` for full context\n")
sb.WriteString("\n\n")
- sb.WriteString("**Requirements**:\n")
- sb.WriteString("- All functions must be < 50 lines\n")
- sb.WriteString("- Must pass `make lint` and `make test`\n")
- sb.WriteString("- Use context-first APIs: `logger.FromContext(ctx)`, `config.FromContext(ctx)`\n")
- sb.WriteString("- No `context.Background()` in runtime code (use `t.Context()` in tests)\n")
- sb.WriteString("- Proper error wrapping with `fmt.Errorf` and `%w`\n")
- sb.WriteString("- Follow SOLID principles and clean architecture patterns\n\n")
+ return sb.String()
+}
+
+func buildCompletionCriteriaSection(taskAbsPath, tasksFile, taskName string) string {
+ var sb strings.Builder
sb.WriteString("## Completion Criteria\n\n")
- sb.WriteString("After implementation:\n")
- sb.WriteString("1. All subtasks in the task file are completed\n")
- sb.WriteString("2. All deliverables are produced\n")
- sb.WriteString("3. All tests pass: `make test`\n")
- sb.WriteString("4. Code passes linting: `make lint`\n")
- sb.WriteString(fmt.Sprintf("5. Update task status in `%s` to `completed`\n", task.absPath))
- sb.WriteString("6. Commit changes with descriptive message referencing the task number\n\n")
+ sb.WriteString("After implementation, you MUST complete ALL of the following steps:\n\n")
+ sb.WriteString("1. **Verify Implementation**:\n")
+ sb.WriteString(" - All subtasks in this task file are completed\n")
+ sb.WriteString(" - All deliverables specified are produced\n")
+ sb.WriteString(" - All tests pass: `make test`\n")
+ sb.WriteString(" - Code passes linting: `make lint`\n\n")
+ sb.WriteString("2. **Mark Subtasks Complete**:\n")
+ sb.WriteString(fmt.Sprintf(" - In `%s`, check all `[ ]` boxes to `[x]` for completed subtasks\n\n", taskAbsPath))
+ sb.WriteString("3. **Update Task Status**:\n")
+ sb.WriteString(fmt.Sprintf(" - In `%s`, change the status line from:\n", taskAbsPath))
+ sb.WriteString(" ```\n")
+ sb.WriteString(" ## status: pending\n")
+ sb.WriteString(" ```\n")
+ sb.WriteString(" to:\n")
+ sb.WriteString(" ```\n")
+ sb.WriteString(" ## status: completed\n")
+ sb.WriteString(" ```\n\n")
+ sb.WriteString("4. **Update Master Tasks List**:\n")
+ sb.WriteString(fmt.Sprintf(" - In `%s`, check the corresponding task checkbox for `%s`\n\n", tasksFile, taskName))
+ sb.WriteString("5. **Commit Changes**:\n")
+ sb.WriteString(
+ fmt.Sprintf(" - Commit all changes with a descriptive message like: `feat: complete %s`\n\n", taskName),
+ )
+ sb.WriteString("\n")
+ sb.WriteString("**DO NOT SKIP ANY COMPLETION STEPS**\n")
+ sb.WriteString("Your task is NOT complete until ALL steps above are done, including:\n")
+ sb.WriteString("- All subtask checkboxes marked\n")
+ sb.WriteString("- Status changed to 'completed'\n")
+ sb.WriteString("- Master tasks list updated\n")
+ sb.WriteString("\n\n")
return sb.String()
}
@@ -3175,22 +3605,66 @@ func (r *lineRing) snapshot() []string {
return out
}
+// activityMonitor tracks the last time output was received from a process.
+// It enables activity-based timeout detection, where a job is considered
+// stuck if no output is received within the configured timeout period.
+type activityMonitor struct {
+ mu sync.Mutex
+ lastActivity time.Time
+}
+
+func newActivityMonitor() *activityMonitor {
+ return &activityMonitor{
+ lastActivity: time.Now(),
+ }
+}
+
+func (a *activityMonitor) recordActivity() {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ a.lastActivity = time.Now()
+}
+
+func (a *activityMonitor) timeSinceLastActivity() time.Duration {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+ return time.Since(a.lastActivity)
+}
+
// uiLogTap is an io.Writer that splits by newlines, appends to a ring buffer
// and emits UI updates with the newest snapshots.
type uiLogTap struct {
- idx int
- isErr bool
- out *lineRing
- err *lineRing
- ch chan<- uiMsg
- buf []byte
-}
-
-func newUILogTap(idx int, isErr bool, outRing, errRing *lineRing, ch chan<- uiMsg) *uiLogTap {
- return &uiLogTap{idx: idx, isErr: isErr, out: outRing, err: errRing, ch: ch, buf: make([]byte, 0, 1024)}
+ idx int
+ isErr bool
+ out *lineRing
+ err *lineRing
+ ch chan<- uiMsg
+ buf []byte
+ activityMonitor *activityMonitor
+}
+
+func newUILogTap(
+ idx int,
+ isErr bool,
+ outRing, errRing *lineRing,
+ ch chan<- uiMsg,
+ monitor *activityMonitor,
+) *uiLogTap {
+ return &uiLogTap{
+ idx: idx,
+ isErr: isErr,
+ out: outRing,
+ err: errRing,
+ ch: ch,
+ buf: make([]byte, 0, 1024),
+ activityMonitor: monitor,
+ }
}
func (t *uiLogTap) Write(p []byte) (int, error) {
+ if len(p) > 0 && t.activityMonitor != nil {
+ t.activityMonitor.recordActivity()
+ }
cleaned := bytes.ReplaceAll(p, []byte{'\r'}, []byte{'\n'})
t.buf = append(t.buf, cleaned...)
for {
@@ -3217,16 +3691,29 @@ func (t *uiLogTap) Write(p []byte) (int, error) {
// Non-JSON lines are passed through unchanged.
// For Claude messages, it can optionally parse and report token usage via callback.
type jsonFormatter struct {
- w io.Writer
- buf []byte
- usageCallback func(TokenUsage) // Called when Claude usage data is parsed
+ w io.Writer
+ buf []byte
+ usageCallback func(TokenUsage) // Called when Claude usage data is parsed
+ activityMonitor *activityMonitor
}
-func newJSONFormatterWithCallback(w io.Writer, callback func(TokenUsage)) *jsonFormatter {
- return &jsonFormatter{w: w, buf: make([]byte, 0, 4096), usageCallback: callback}
+func newJSONFormatterWithCallbackAndMonitor(
+ w io.Writer,
+ callback func(TokenUsage),
+ monitor *activityMonitor,
+) *jsonFormatter {
+ return &jsonFormatter{
+ w: w,
+ buf: make([]byte, 0, 4096),
+ usageCallback: callback,
+ activityMonitor: monitor,
+ }
}
func (f *jsonFormatter) Write(p []byte) (int, error) {
+ if len(p) > 0 && f.activityMonitor != nil {
+ f.activityMonitor.recordActivity()
+ }
f.buf = append(f.buf, p...)
for {
i := bytes.IndexByte(f.buf, '\n')
diff --git a/tasks/prd-postgres/_docs.md b/tasks/prd-postgres/_docs.md
new file mode 100644
index 00000000..365a2adb
--- /dev/null
+++ b/tasks/prd-postgres/_docs.md
@@ -0,0 +1,409 @@
+# Documentation Plan: SQLite Database Backend Support
+
+## Goals
+
+- Provide comprehensive documentation for database selection and configuration
+- Create decision-making guides for when to use PostgreSQL vs SQLite
+- Document vector database requirements for SQLite mode
+- Update existing pages to reflect multi-database support
+- Ensure smooth user experience for both database options
+
+## New/Updated Pages
+
+### 1. `docs/content/docs/database/overview.mdx` (NEW)
+
+- **Purpose:** High-level database overview and decision guide
+- **Outline:**
+ - Introduction: Multi-database support in Compozy
+ - Database Options Comparison Table
+ - Decision Matrix: When to use PostgreSQL vs SQLite
+ - Architecture Overview (both drivers)
+ - Migration Considerations
+- **Links:**
+ - β `database/postgresql.mdx`
+ - β `database/sqlite.mdx`
+ - β `configuration/database.mdx`
+ - β `getting-started/installation.mdx`
+
+### 2. `docs/content/docs/database/postgresql.mdx` (NEW)
+
+- **Purpose:** PostgreSQL-specific configuration and best practices
+- **Outline:**
+ - PostgreSQL Features and Benefits
+ - Configuration Options
+ - Connection string format
+ - Individual parameters (host, port, user, etc.)
+ - SSL/TLS configuration
+ - Connection pooling settings
+ - pgvector for Knowledge Bases
+ - Performance Tuning
+ - Production Deployment Guide
+ - Troubleshooting Common Issues
+- **Links:**
+ - β `database/overview.mdx`
+ - β `knowledge-bases/vector-databases.mdx`
+ - β `examples/` (PostgreSQL examples)
+
+### 3. `docs/content/docs/database/sqlite.mdx` (NEW)
+
+- **Purpose:** SQLite-specific configuration and use cases
+- **Outline:**
+ - SQLite Features and Limitations
+ - Ideal Use Cases
+ - Development and testing
+ - Edge deployments
+ - Single-tenant scenarios
+ - Configuration Options
+ - File-based database
+ - In-memory mode (`:memory:`)
+ - PRAGMA settings
+ - **CRITICAL: Vector Database Requirement**
+ - Why external vector DB is required
+ - Supported options (Qdrant, Redis, Filesystem)
+ - Configuration examples
+ - Performance Characteristics
+ - Concurrency Limitations (5-10 workflows recommended)
+ - Backup and Export
+ - Troubleshooting
+- **Links:**
+ - β `database/overview.mdx`
+ - β `knowledge-bases/vector-databases.mdx`
+ - β `examples/` (SQLite examples)
+
+### 4. `docs/content/docs/configuration/database.mdx` (UPDATE)
+
+- **Purpose:** Database configuration reference
+- **Outline:**
+ - Add `driver` field documentation
+ - PostgreSQL configuration section
+ - SQLite configuration section
+ - Vector database validation rules
+ - Environment variables
+ - CLI flags
+ - Configuration precedence
+- **Links:**
+ - β `database/overview.mdx`
+ - β Schema reference for `DatabaseConfig`
+
+### 5. `docs/content/docs/knowledge-bases/vector-databases.mdx` (UPDATE)
+
+- **Purpose:** Update to mention SQLite requirements
+- **Updates:**
+ - Add note: "When using SQLite, external vector database is mandatory"
+ - Update pgvector section: "Available with PostgreSQL only"
+ - Add decision matrix for vector DB selection
+ - Configuration examples for each provider
+- **Links:**
+ - β `database/sqlite.mdx`
+ - β `database/postgresql.mdx`
+
+### 6. `docs/content/docs/getting-started/installation.mdx` (UPDATE)
+
+- **Purpose:** Update installation guide to cover both databases
+- **Updates:**
+ - Add "Quick Start with SQLite" section
+ - Update "Production Setup" to emphasize PostgreSQL
+ - Add database selection step
+ - Update CLI examples with `--db-driver` flag
+- **Links:**
+ - β `database/overview.mdx`
+ - β `database/sqlite.mdx`
+ - β `database/postgresql.mdx`
+
+### 7. `docs/content/docs/getting-started/quickstart.mdx` (UPDATE)
+
+- **Purpose:** Add SQLite quick start path
+- **Updates:**
+ - Add "5-Minute Setup (SQLite)" section
+ - Show simplified configuration for local development
+ - Mention when to switch to PostgreSQL
+- **Links:**
+ - β `database/overview.mdx`
+ - β Tutorial for first workflow
+
+### 8. `docs/content/docs/deployment/production.mdx` (UPDATE)
+
+- **Purpose:** Emphasize PostgreSQL for production
+- **Updates:**
+ - Add "Database Selection" section
+ - Strong recommendation for PostgreSQL in production
+ - Performance comparison table
+ - Scaling considerations
+- **Links:**
+ - β `database/postgresql.mdx`
+ - β `deployment/scaling.mdx`
+
+### 9. `docs/content/docs/troubleshooting/database.mdx` (NEW)
+
+- **Purpose:** Database-specific troubleshooting guide
+- **Outline:**
+ - Common PostgreSQL Issues
+ - Connection failures
+ - Migration errors
+ - Performance problems
+ - Common SQLite Issues
+ - Database locked errors
+ - Concurrency problems
+ - File permission issues
+ - Vector DB not configured
+ - Diagnostic Commands
+ - Error Messages Reference
+- **Links:**
+ - β `database/postgresql.mdx`
+ - β `database/sqlite.mdx`
+
+## Schema Docs
+
+### 1. `docs/content/docs/reference/configuration/database.mdx` (UPDATE)
+
+- **Renders:** `schemas/config-database.json` (updated with `driver` field)
+- **Notes:**
+ - Highlight `driver` field as key selection mechanism
+ - Document PostgreSQL-specific vs SQLite-specific fields
+ - Show validation rules (e.g., SQLite + pgvector not allowed)
+
+## API Docs
+
+No API changes - database selection is configuration-driven. No new API endpoints.
+
+## CLI Docs
+
+### 1. `docs/content/docs/cli/start.mdx` (UPDATE)
+
+- **Updates:**
+ - Add `--db-driver` flag documentation
+ - Add `--db-path` flag for SQLite
+ - Update examples to show both PostgreSQL and SQLite usage
+- **Example:**
+ ```bash
+ # PostgreSQL (default)
+ compozy start
+
+ # SQLite (file-based)
+ compozy start --db-driver=sqlite --db-path=./compozy.db
+
+ # SQLite (in-memory)
+ compozy start --db-driver=sqlite --db-path=:memory:
+ ```
+
+### 2. `docs/content/docs/cli/migrate.mdx` (UPDATE)
+
+- **Updates:**
+ - Document migration behavior for both drivers
+ - Show how to run migrations manually
+ - Explain dual migration files
+- **Commands:**
+ ```bash
+ # Apply migrations (auto-detects driver from config)
+ compozy migrate up
+
+ # Check migration status
+ compozy migrate status
+
+ # Rollback migrations
+ compozy migrate down
+ ```
+
+### 3. `docs/content/docs/cli/config.mdx` (UPDATE)
+
+- **Updates:**
+ - Add `database` section examples
+ - Show both PostgreSQL and SQLite configurations
+ - Document environment variable overrides
+
+## Cross-page Updates
+
+### `docs/content/docs/index.mdx` (Homepage)
+
+- **Update:** Add mention of multi-database support in feature list
+- **Note:** "Flexible database options: PostgreSQL (production) or SQLite (development/edge)"
+
+### `docs/content/docs/concepts/architecture.mdx`
+
+- **Update:** Add section on "Storage Layer" explaining database abstraction
+- **Diagram:** Show repository pattern with both drivers
+
+### `docs/content/docs/examples/index.mdx`
+
+- **Update:** Group examples by database type or show dual configs
+- **Note:** Indicate which examples work with SQLite vs PostgreSQL
+
+## Navigation & Indexing
+
+### Update `docs/source.config.ts`
+
+**New Section Structure:**
+
+```typescript
+{
+ title: "Database",
+ pages: [
+ "database/overview", // NEW
+ "database/postgresql", // NEW
+ "database/sqlite", // NEW
+ ]
+},
+{
+ title: "Configuration",
+ pages: [
+ "configuration/overview",
+ "configuration/database", // UPDATE
+ "configuration/workflows",
+ // ... existing pages
+ ]
+},
+{
+ title: "Troubleshooting",
+ pages: [
+ "troubleshooting/overview",
+ "troubleshooting/database", // NEW
+ "troubleshooting/workflows",
+ // ... existing pages
+ ]
+}
+```
+
+**Sidebar Order:**
+1. Getting Started
+2. Concepts
+3. **Database** (new section)
+4. Configuration
+5. Workflows
+6. Agents
+7. Tools
+8. Knowledge Bases
+9. Examples
+10. CLI Reference
+11. Troubleshooting
+
+## Decision Matrix Template
+
+**Include in `database/overview.mdx`:**
+
+| Criterion | PostgreSQL | SQLite |
+|-----------|-----------|--------|
+| **Use Case** | Production, Multi-tenant | Development, Edge, Single-tenant |
+| **Concurrency** | High (100+ workflows) | Low (5-10 workflows) |
+| **Scalability** | Excellent (horizontal/vertical) | Limited (single file) |
+| **Vector Search** | pgvector (built-in) | External DB required |
+| **Deployment** | Separate database server | Embedded (single binary) |
+| **Setup Complexity** | Moderate (requires PostgreSQL) | Low (just a file path) |
+| **Performance** | Optimized for high load | Optimized for reads |
+| **Backup** | PostgreSQL tools (pg_dump) | File copy |
+| **Recommended For** | β
Production deployments | β
Development, testing, edge |
+
+## Code Snippets
+
+### PostgreSQL Configuration Example
+
+```yaml
+# compozy.yaml
+database:
+ driver: postgres # default, can be omitted
+ host: localhost
+ port: 5432
+ user: compozy
+ password: ${DB_PASSWORD} # from env
+ dbname: compozy
+ sslmode: require
+ max_open_conns: 25
+ max_idle_conns: 5
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: pgvector # Uses PostgreSQL
+ dimension: 1536
+```
+
+### SQLite Configuration Example
+
+```yaml
+# compozy.yaml
+database:
+ driver: sqlite
+ path: ./data/compozy.db # or :memory: for in-memory
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: qdrant # External vector DB required
+ url: http://localhost:6333
+ dimension: 1536
+```
+
+## Visual Assets
+
+### Diagrams to Create
+
+1. **Database Architecture Diagram** (`database/overview.mdx`)
+ - Show dual driver architecture
+ - Repository pattern with factory selection
+ - Domain layer independence
+
+2. **Decision Flowchart** (`database/overview.mdx`)
+ - "Which database should I use?"
+ - Decision tree based on use case, scale, deployment type
+
+3. **Vector DB Options** (`database/sqlite.mdx`)
+ - Show SQLite β External Vector DB connection
+ - Compare Qdrant, Redis, Filesystem options
+
+## Acceptance Criteria
+
+- [ ] All new pages created with complete outlines
+- [ ] All update pages modified with SQLite references
+- [ ] Schema documentation reflects new `driver` field
+- [ ] CLI reference includes database flags
+- [ ] Decision matrix helps users choose database
+- [ ] Vector DB requirement for SQLite clearly documented
+- [ ] Navigation structure updated in `source.config.ts`
+- [ ] Code examples valid and tested
+- [ ] Internal links between pages work
+- [ ] Docs dev server builds without errors (`npm run dev`)
+- [ ] Search index includes new database terms
+- [ ] Mobile view renders correctly
+- [ ] Dark mode styling consistent
+
+## Testing Documentation
+
+### Manual Testing Checklist
+
+- [ ] Follow PostgreSQL setup guide β successful workflow execution
+- [ ] Follow SQLite setup guide β successful workflow execution
+- [ ] Try invalid config (SQLite + pgvector) β clear error message
+- [ ] Copy/paste code examples β they work as-is
+- [ ] Click all internal links β no 404s
+- [ ] Search for "database" β relevant results
+- [ ] Search for "sqlite" β new pages appear
+
+### Automated Checks
+
+- [ ] Link checker passes (no broken internal links)
+- [ ] Code block syntax highlighting works
+- [ ] Schema references resolve correctly
+- [ ] Build process completes without warnings
+
+## Documentation Timeline
+
+| Week | Deliverable |
+|------|-------------|
+| 1-2 | Database overview, PostgreSQL/SQLite pages (draft) |
+| 3 | Configuration reference, CLI updates |
+| 4 | Troubleshooting guide, cross-page updates |
+| 5 | Review, editing, diagrams |
+| 6 | Final review, publication |
+
+## Notes
+
+- **Priority:** High - Documentation crucial for adoption
+- **Audience:** Mixed (beginners need simplicity, experts need depth)
+- **Tone:** Practical, prescriptive (guide users to right choice)
+- **Maintenance:** Update when adding new features to either driver
+- **Feedback:** Collect user feedback on clarity of database selection guide
+
+---
+
+**Plan Version:** 1.0
+**Date:** 2025-01-27
+**Status:** Ready for Implementation
diff --git a/tasks/prd-postgres/_examples.md b/tasks/prd-postgres/_examples.md
new file mode 100644
index 00000000..5ac3e99a
--- /dev/null
+++ b/tasks/prd-postgres/_examples.md
@@ -0,0 +1,608 @@
+# Examples Plan: SQLite Database Backend Support
+
+## Conventions
+
+- Folder prefix: `examples/database/*`
+- Use environment variable interpolation for sensitive data
+- Provide clear README with prerequisites and expected outputs
+- Show both PostgreSQL and SQLite configurations where applicable
+
+## Example Matrix
+
+### 1. `examples/database/sqlite-quickstart`
+
+- **Purpose:** Demonstrate fastest way to get started with SQLite
+- **Files:**
+ - `compozy.yaml` β Minimal SQLite configuration
+ - `workflows/hello-world.yaml` β Simple workflow
+ - `README.md` β Setup instructions and walkthrough
+ - `.env.example` β No external dependencies
+- **Demonstrates:**
+ - SQLite file-based database
+ - Single-binary deployment
+ - No external services required (except LLM API)
+ - Filesystem vector DB for knowledge features
+- **Walkthrough:**
+ ```bash
+ cd examples/database/sqlite-quickstart
+ cp .env.example .env
+ # Edit .env to add your OpenAI API key
+ compozy start
+ # Database automatically created at ./data/compozy.db
+ compozy workflow run hello-world
+ ```
+
+### 2. `examples/database/sqlite-memory`
+
+- **Purpose:** Show in-memory SQLite for testing/ephemeral workloads
+- **Files:**
+ - `compozy.yaml` β In-memory SQLite (`:memory:`)
+ - `workflows/test-workflow.yaml` β Test workflow
+ - `README.md` β Use case explanation
+- **Demonstrates:**
+ - In-memory database (no persistence)
+ - Fastest startup time
+ - Perfect for CI/CD tests
+ - No vector DB required (no knowledge features)
+- **Walkthrough:**
+ ```bash
+ cd examples/database/sqlite-memory
+ compozy start
+ # Database exists only in memory, lost on restart
+ compozy workflow run test-workflow
+ ```
+
+### 3. `examples/database/sqlite-qdrant`
+
+- **Purpose:** SQLite with Qdrant for knowledge bases
+- **Files:**
+ - `compozy.yaml` β SQLite + Qdrant configuration
+ - `workflows/rag-workflow.yaml` β RAG workflow
+ - `knowledge/documents/*.md` β Sample documents
+ - `docker-compose.yml` β Qdrant service
+ - `README.md` β Setup guide
+ - `.env.example` β Qdrant URL
+- **Demonstrates:**
+ - SQLite for relational data
+ - Qdrant for vector embeddings
+ - Knowledge base ingestion and querying
+ - Hybrid deployment (embedded SQLite + external Qdrant)
+- **Walkthrough:**
+ ```bash
+ cd examples/database/sqlite-qdrant
+ docker-compose up -d # Start Qdrant
+ cp .env.example .env
+ compozy knowledge ingest ./knowledge
+ compozy start
+ compozy workflow run rag-workflow --input='{"query": "What is AI?"}'
+ ```
+
+### 4. `examples/database/postgres-comparison`
+
+- **Purpose:** Side-by-side PostgreSQL vs SQLite comparison
+- **Files:**
+ - `compozy-postgres.yaml` β PostgreSQL config
+ - `compozy-sqlite.yaml` β SQLite config
+ - `workflows/benchmark.yaml` β Performance test workflow
+ - `docker-compose.yml` β PostgreSQL service
+ - `README.md` β Comparison guide
+ - `benchmark.sh` β Script to run both and compare
+- **Demonstrates:**
+ - Same workflow, different databases
+ - Performance characteristics
+ - When to choose which database
+- **Walkthrough:**
+ ```bash
+ cd examples/database/postgres-comparison
+ docker-compose up -d # Start PostgreSQL
+
+ # Run with PostgreSQL
+ ./benchmark.sh --config=compozy-postgres.yaml
+
+ # Run with SQLite
+ ./benchmark.sh --config=compozy-sqlite.yaml
+
+ # Compare results
+ ```
+
+### 5. `examples/database/migration-export-import`
+
+- **Purpose:** Show data export/import between databases
+- **Files:**
+ - `compozy-source.yaml` β Source database config
+ - `compozy-target.yaml` β Target database config
+ - `workflows/sample-data.yaml` β Generate test data
+ - `scripts/export.sh` β Export script
+ - `scripts/import.sh` β Import script
+ - `README.md` β Migration guide
+- **Demonstrates:**
+ - Exporting workflow/task state to JSON
+ - Importing state to different database
+ - Use case: PostgreSQL β SQLite or vice versa
+- **Walkthrough:**
+ ```bash
+ cd examples/database/migration-export-import
+
+ # Run workflows on source DB
+ compozy --config=compozy-source.yaml workflow run sample-data
+
+ # Export data
+ ./scripts/export.sh > backup.json
+
+ # Import to target DB
+ ./scripts/import.sh backup.json
+
+ # Verify
+ compozy --config=compozy-target.yaml workflow list
+ ```
+
+### 6. `examples/database/edge-deployment`
+
+- **Purpose:** Demonstrate edge/IoT deployment with SQLite
+- **Files:**
+ - `compozy.yaml` β Minimal SQLite config
+ - `workflows/sensor-processing.yaml` β Edge workflow
+ - `Dockerfile` β Single-binary container
+ - `README.md` β Edge deployment guide
+- **Demonstrates:**
+ - Minimal dependencies (no PostgreSQL)
+ - Single binary + SQLite file
+ - Docker container <100MB
+ - Suitable for ARM devices
+- **Walkthrough:**
+ ```bash
+ cd examples/database/edge-deployment
+
+ # Build minimal image
+ docker build -t compozy-edge .
+
+ # Run on edge device
+ docker run -v $(pwd)/data:/data compozy-edge
+ ```
+
+## Minimal YAML Shapes
+
+### SQLite Quickstart Config
+
+```yaml
+# compozy.yaml (minimal SQLite)
+version: "1.0"
+
+database:
+ driver: sqlite
+ path: ./data/compozy.db
+
+llm:
+ providers:
+ - id: openai
+ type: openai
+ api_key: ${OPENAI_API_KEY}
+
+server:
+ port: 8080
+```
+
+### SQLite + Qdrant Config
+
+```yaml
+# compozy.yaml (SQLite with external vector DB)
+version: "1.0"
+
+database:
+ driver: sqlite
+ path: ./data/compozy.db
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: qdrant
+ url: ${QDRANT_URL:-http://localhost:6333}
+ dimension: 1536
+
+llm:
+ providers:
+ - id: openai
+ type: openai
+ api_key: ${OPENAI_API_KEY}
+```
+
+### PostgreSQL Config (for comparison)
+
+```yaml
+# compozy.yaml (PostgreSQL)
+version: "1.0"
+
+database:
+ driver: postgres
+ host: ${DB_HOST:-localhost}
+ port: 5432
+ user: ${DB_USER:-compozy}
+ password: ${DB_PASSWORD}
+ dbname: ${DB_NAME:-compozy}
+ sslmode: ${DB_SSLMODE:-disable}
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: pgvector # Uses PostgreSQL
+ dimension: 1536
+
+llm:
+ providers:
+ - id: openai
+ type: openai
+ api_key: ${OPENAI_API_KEY}
+```
+
+### Docker Compose - Qdrant
+
+```yaml
+# docker-compose.yml (Qdrant service)
+version: "3.8"
+
+services:
+ qdrant:
+ image: qdrant/qdrant:latest
+ ports:
+ - "6333:6333"
+ volumes:
+ - qdrant_data:/qdrant/storage
+ environment:
+ - QDRANT_LOG_LEVEL=INFO
+
+volumes:
+ qdrant_data:
+```
+
+### Docker Compose - PostgreSQL + pgAdmin
+
+```yaml
+# docker-compose.yml (PostgreSQL comparison example)
+version: "3.8"
+
+services:
+ postgres:
+ image: pgvector/pgvector:pg16
+ ports:
+ - "5432:5432"
+ environment:
+ - POSTGRES_USER=compozy
+ - POSTGRES_PASSWORD=compozy
+ - POSTGRES_DB=compozy
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+
+ pgadmin:
+ image: dpage/pgadmin4:latest
+ ports:
+ - "5050:80"
+ environment:
+ - PGADMIN_DEFAULT_EMAIL=admin@compozy.com
+ - PGADMIN_DEFAULT_PASSWORD=admin
+
+volumes:
+ postgres_data:
+```
+
+## Test & CI Coverage
+
+### Integration Tests to Add
+
+- **`test/integration/database/sqlite_test.go`**
+ - Test SQLite-specific workflows
+ - Verify file-based vs in-memory behavior
+ - Test concurrent workflow execution (within SQLite limits)
+
+- **`test/integration/database/multi_driver_test.go`**
+ - Parameterized tests running against both drivers
+ - Validate same workflow behavior regardless of database
+ - Compare performance characteristics
+
+- **`test/integration/database/vector_validation_test.go`**
+ - Test SQLite + pgvector rejection (should fail)
+ - Test SQLite + Qdrant acceptance (should pass)
+ - Test PostgreSQL + pgvector acceptance (should pass)
+
+### CI/CD Matrix
+
+```yaml
+# .github/workflows/database-examples.yml
+name: Database Examples
+
+on: [push, pull_request]
+
+jobs:
+ test-sqlite-examples:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v4
+ with:
+ go-version: "1.25.2"
+
+ - name: Test SQLite Quickstart
+ run: |
+ cd examples/database/sqlite-quickstart
+ compozy start &
+ sleep 5
+ compozy workflow run hello-world
+
+ - name: Test SQLite Memory
+ run: |
+ cd examples/database/sqlite-memory
+ compozy start &
+ sleep 5
+ compozy workflow run test-workflow
+
+ test-postgres-examples:
+ runs-on: ubuntu-latest
+ services:
+ postgres:
+ image: pgvector/pgvector:pg16
+ env:
+ POSTGRES_PASSWORD: compozy
+ POSTGRES_USER: compozy
+ POSTGRES_DB: compozy
+ ports:
+ - 5432:5432
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v4
+ with:
+ go-version: "1.25.2"
+
+ - name: Test PostgreSQL Example
+ run: |
+ cd examples/database/postgres-comparison
+ compozy start --config=compozy-postgres.yaml &
+ sleep 5
+ compozy workflow run benchmark
+```
+
+## Runbooks per Example
+
+### SQLite Quickstart
+
+**Prerequisites:**
+- Compozy CLI installed
+- OpenAI API key (or other LLM provider)
+
+**Commands:**
+```bash
+# 1. Navigate to example
+cd examples/database/sqlite-quickstart
+
+# 2. Configure
+cp .env.example .env
+# Edit .env: Set OPENAI_API_KEY=sk-...
+
+# 3. Start Compozy (database auto-created)
+compozy start
+
+# 4. Run workflow (in another terminal)
+compozy workflow run hello-world
+
+# 5. Verify database created
+ls -lh ./data/compozy.db
+
+# 6. View workflow status
+compozy workflow list
+
+# 7. Stop server
+# Ctrl+C in terminal running compozy start
+```
+
+**Expected Output:**
+```
+Starting Compozy server...
+Database initialized: driver=sqlite path=./data/compozy.db
+Server listening on :8080
+
+Workflow 'hello-world' completed successfully
+Result: { ... }
+```
+
+---
+
+### SQLite + Qdrant (Knowledge Base)
+
+**Prerequisites:**
+- Docker and Docker Compose
+- Compozy CLI installed
+- OpenAI API key
+
+**Commands:**
+```bash
+# 1. Navigate to example
+cd examples/database/sqlite-qdrant
+
+# 2. Start Qdrant
+docker-compose up -d
+
+# 3. Verify Qdrant is running
+curl http://localhost:6333/healthz
+
+# 4. Configure
+cp .env.example .env
+# Edit .env: Set OPENAI_API_KEY=sk-...
+
+# 5. Ingest knowledge
+compozy knowledge ingest ./knowledge
+
+# 6. Start Compozy
+compozy start
+
+# 7. Run RAG workflow
+compozy workflow run rag-workflow \
+ --input='{"query": "What is machine learning?"}'
+
+# 8. Cleanup
+docker-compose down
+```
+
+**Expected Output:**
+```
+Ingesting documents from ./knowledge...
+β Processed 5 documents
+β Created 42 chunks
+β Embeddings stored in Qdrant
+
+Workflow 'rag-workflow' completed
+Answer: Machine learning is a subset of artificial intelligence...
+Sources: [doc1.md, doc2.md]
+```
+
+---
+
+### PostgreSQL vs SQLite Comparison
+
+**Prerequisites:**
+- Docker and Docker Compose
+- Compozy CLI installed
+
+**Commands:**
+```bash
+# 1. Navigate to example
+cd examples/database/postgres-comparison
+
+# 2. Start PostgreSQL
+docker-compose up -d
+
+# 3. Run benchmark with PostgreSQL
+./benchmark.sh --config=compozy-postgres.yaml
+
+# 4. Run benchmark with SQLite
+./benchmark.sh --config=compozy-sqlite.yaml
+
+# 5. Compare results
+cat results-postgres.json
+cat results-sqlite.json
+
+# 6. Cleanup
+docker-compose down
+```
+
+**Expected Output:**
+```
+Running benchmark with PostgreSQL...
+10 workflows executed in 2.3s
+Average latency: 230ms
+Peak concurrency: 25 workflows
+
+Running benchmark with SQLite...
+10 workflows executed in 3.1s
+Average latency: 310ms
+Peak concurrency: 8 workflows
+```
+
+## Acceptance Criteria
+
+- [ ] P0 examples (sqlite-quickstart, sqlite-qdrant) runnable locally
+- [ ] README in each folder with clear instructions
+- [ ] All code examples tested and working
+- [ ] Docker Compose files validated (services start successfully)
+- [ ] Environment variables documented in `.env.example`
+- [ ] Expected outputs documented in READMEs
+- [ ] CI/CD tests pass for all examples
+- [ ] No hardcoded secrets or credentials
+- [ ] Examples demonstrate key use cases:
+ - [ ] Quick local development (SQLite file)
+ - [ ] Testing/CI (SQLite memory)
+ - [ ] Knowledge bases (SQLite + external vector DB)
+ - [ ] Performance comparison (PostgreSQL vs SQLite)
+ - [ ] Data migration (export/import)
+ - [ ] Edge deployment (minimal footprint)
+- [ ] Cross-references to documentation pages
+
+## Additional Resources
+
+### Sample Workflows
+
+**`workflows/hello-world.yaml`** (for SQLite quickstart):
+```yaml
+id: hello-world
+name: Hello World Workflow
+
+tasks:
+ - id: greet
+ prompt: "Say hello to {{ .input.name | default \"World\" }}"
+ agent:
+ llm: openai
+
+output:
+ greeting: "{{ .tasks.greet.output.text }}"
+```
+
+**`workflows/rag-workflow.yaml`** (for SQLite + Qdrant):
+```yaml
+id: rag-workflow
+name: RAG Workflow with Knowledge Base
+
+tasks:
+ - id: retrieve
+ prompt: "Find relevant context for: {{ .input.query }}"
+ agent:
+ llm: openai
+ knowledge:
+ - knowledge_base_id: main
+ top_k: 5
+
+ - id: answer
+ prompt: |
+ Based on the following context:
+ {{ .tasks.retrieve.output.text }}
+
+ Answer: {{ .input.query }}
+ agent:
+ llm: openai
+
+output:
+ answer: "{{ .tasks.answer.output.text }}"
+ sources: "{{ .tasks.retrieve.output.sources }}"
+```
+
+### Benchmark Script
+
+**`scripts/benchmark.sh`:**
+```bash
+#!/bin/bash
+
+CONFIG=${1:-compozy.yaml}
+WORKFLOWS=${2:-10}
+
+echo "Running benchmark with config: $CONFIG"
+echo "Number of workflows: $WORKFLOWS"
+
+# Start server in background
+compozy --config="$CONFIG" start &
+PID=$!
+sleep 5
+
+# Record start time
+START=$(date +%s)
+
+# Run workflows
+for i in $(seq 1 $WORKFLOWS); do
+ compozy workflow run benchmark &
+done
+
+# Wait for all workflows
+wait
+
+# Record end time
+END=$(date +%s)
+DURATION=$((END - START))
+
+echo "Benchmark complete"
+echo "Total time: ${DURATION}s"
+echo "Average: $(( DURATION * 1000 / WORKFLOWS ))ms per workflow"
+
+# Stop server
+kill $PID
+```
+
+---
+
+**Plan Version:** 1.0
+**Date:** 2025-01-27
+**Status:** Ready for Implementation
diff --git a/tasks/prd-postgres/_prd.md b/tasks/prd-postgres/_prd.md
new file mode 100644
index 00000000..9c91b1e8
--- /dev/null
+++ b/tasks/prd-postgres/_prd.md
@@ -0,0 +1,381 @@
+# PRD: SQLite Database Backend Support
+
+## Overview
+
+Add SQLite as an alternative database backend to PostgreSQL for Compozy, enabling single-binary deployments and simplified development environments while maintaining PostgreSQL as the default and recommended option for production workloads.
+
+## Problem Statement
+
+Compozy currently requires PostgreSQL as a mandatory dependency, which creates barriers for:
+
+1. **Development & Testing:** Developers need to run PostgreSQL locally or via Docker, adding complexity to onboarding
+2. **Edge Deployments:** Embedded/IoT scenarios where running a separate database server is impractical
+3. **Single-Binary Distribution:** Users wanting a truly self-contained binary without external database dependencies
+4. **Quick Evaluation:** Potential users want to try Compozy without setting up infrastructure
+5. **CI/CD Complexity:** Test environments require PostgreSQL setup, slowing down pipelines
+
+## Goals
+
+### Primary Goals
+
+1. **Enable SQLite Backend:** Support SQLite 3.x as an alternative database driver alongside PostgreSQL
+2. **Maintain PostgreSQL Default:** Keep PostgreSQL as the recommended production database
+3. **Zero Breaking Changes:** Existing PostgreSQL configurations continue working unchanged
+4. **Clear Guidance:** Document when to use SQLite vs PostgreSQL based on use case
+5. **Vector DB Flexibility:** Mandate external vector database (Qdrant/Redis/Filesystem) when using SQLite
+
+### Non-Goals
+
+1. **Migration Tools:** Auto-migration from PostgreSQL to SQLite (manual export/import acceptable)
+2. **Feature Parity:** SQLite doesn't need to match PostgreSQL's concurrency/performance characteristics
+3. **Default Change:** PostgreSQL remains the default driver
+4. **Backwards Compatibility:** No need to support upgrading old SQLite databases
+
+## Success Metrics
+
+### Quantitative Metrics
+
+- **Developer Onboarding Time:** Reduce time-to-first-workflow from 15min β 5min (SQLite mode)
+- **Binary Size:** Self-contained binary remains under 150MB
+- **Test Suite Speed:** Integration tests 30% faster with in-memory SQLite
+- **CI/CD Time:** Reduce test pipeline duration by 20%
+
+### Qualitative Metrics
+
+- Documentation clearly explains SQLite vs PostgreSQL trade-offs
+- Zero production issues from accidental SQLite usage in high-concurrency scenarios
+- Positive community feedback on simplified local development
+
+## User Personas
+
+### Persona 1: New Developer (Primary)
+
+- **Need:** Quick local setup to evaluate Compozy
+- **Pain:** Setting up PostgreSQL is a barrier to getting started
+- **Benefit:** `compozy init` creates local SQLite database, ready immediately
+
+### Persona 2: Edge Deployment Engineer
+
+- **Need:** Deploy Compozy on embedded devices with limited resources
+- **Pain:** Cannot run PostgreSQL on resource-constrained hardware
+- **Benefit:** Single binary with SQLite embedded, no external dependencies
+
+### Persona 3: CI/CD Pipeline Owner
+
+- **Need:** Fast, reliable test execution
+- **Pain:** PostgreSQL test containers slow down CI pipelines
+- **Benefit:** In-memory SQLite for integration tests, faster execution
+
+### Persona 4: Production Operator (Secondary - PostgreSQL)
+
+- **Need:** High-concurrency, scalable production deployment
+- **Current State:** Already using PostgreSQL
+- **Impact:** Zero change; continues using PostgreSQL as recommended
+
+## Requirements
+
+### Functional Requirements
+
+#### FR-1: Database Driver Selection
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Support `database.driver` configuration field with values: `postgres` (default) or `sqlite`
+- **Acceptance Criteria:**
+ - Config validates driver value
+ - PostgreSQL is default when field omitted
+ - Invalid driver values produce clear error messages
+
+#### FR-2: SQLite Driver Implementation
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Implement complete repository pattern for SQLite backend
+- **Acceptance Criteria:**
+ - All workflow state operations work (create, read, update, list)
+ - All task state operations work with hierarchical relationships
+ - All auth operations work (users, API keys)
+ - Transactions with proper isolation levels
+ - Foreign key constraints enforced
+
+#### FR-3: Database-Specific Configuration
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Support driver-specific configuration options
+- **PostgreSQL Config:** conn_string, host, port, user, password, dbname, ssl_mode
+- **SQLite Config:** path (file path or `:memory:`)
+- **Acceptance Criteria:**
+ - PostgreSQL config unchanged
+ - SQLite requires only `path` field
+ - Configuration validation catches missing required fields per driver
+
+#### FR-4: Migration Support
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Database migrations work for both PostgreSQL and SQLite
+- **Acceptance Criteria:**
+ - Dual migration files (PostgreSQL SQL + SQLite SQL)
+ - `compozy migrate up/down` works for both drivers
+ - Migrations run automatically on server start (configurable)
+
+#### FR-5: Vector Database Requirement for SQLite
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** When using SQLite, external vector database is mandatory for knowledge features
+- **Acceptance Criteria:**
+ - Startup validation fails if SQLite + pgvector provider configured
+ - Startup validation passes if SQLite + (Qdrant OR Redis OR Filesystem) configured
+ - Clear error message guiding users to configure external vector DB
+ - Documentation explains vector DB requirement
+
+### Non-Functional Requirements
+
+#### NFR-1: Performance (SQLite)
+
+**Priority:** P1 (Should Have)
+
+- **Requirement:** SQLite performance sufficient for single-tenant, low-concurrency workloads
+- **Targets:**
+ - Read operations: <50ms p99 (comparable to PostgreSQL)
+ - Write operations: <100ms p99 (acceptable degradation)
+ - Concurrent workflows: Support 5-10 simultaneous executions
+- **Acceptance Criteria:**
+ - Benchmark tests demonstrate target performance
+ - Documentation warns about concurrency limitations
+
+#### NFR-2: Compatibility
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Zero breaking changes to existing PostgreSQL deployments
+- **Acceptance Criteria:**
+ - All existing tests pass with PostgreSQL backend
+ - Existing configuration files work unchanged
+ - No schema migrations required for PostgreSQL users
+
+#### NFR-3: Testing Coverage
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Comprehensive test coverage for both database backends
+- **Acceptance Criteria:**
+ - Unit tests: 80%+ coverage for new SQLite code
+ - Integration tests: All critical paths tested with both drivers
+ - CI/CD: Matrix testing (PostgreSQL + SQLite)
+
+#### NFR-4: Documentation Quality
+
+**Priority:** P0 (Must Have)
+
+- **Requirement:** Clear, comprehensive documentation for database selection
+- **Acceptance Criteria:**
+ - Decision matrix: When to use SQLite vs PostgreSQL
+ - Configuration examples for both drivers
+ - Vector DB configuration guide for SQLite
+ - Performance characteristics comparison
+ - Migration guide (if switching databases)
+
+## Technical Constraints
+
+### System Constraints
+
+1. **Go Version:** Must support Go 1.25.2+ (current project version)
+2. **SQLite Version:** Target SQLite 3.38.0+ (for JSON support)
+3. **Driver Choice:** Use `modernc.org/sqlite` (pure Go, no CGO) or `github.com/mattn/go-sqlite3` (CGO, more mature)
+4. **Vector Storage:** No native SQLite vector support - external DB required
+
+### Architecture Constraints
+
+1. **Repository Pattern:** Must maintain existing repository interfaces
+2. **Clean Architecture:** Follow existing domain-driven design patterns
+3. **Dependency Injection:** Context-first configuration and logging (no DI for config/logger)
+4. **No Globals:** Zero global configuration state
+
+### Database Feature Constraints
+
+1. **Row Locking:** SQLite has DB-level locking only (vs PostgreSQL row-level)
+2. **Concurrency:** SQLite write serialization acceptable for target use cases
+3. **JSONB:** Map PostgreSQL JSONB to SQLite JSON functions
+4. **Array Operations:** Convert PostgreSQL `ANY($1::type[])` to `IN (?, ?, ?)`
+
+## Dependencies
+
+### Internal Dependencies
+
+- **Configuration System:** `pkg/config` - add database driver selection
+- **Repository Interfaces:** `engine/workflow`, `engine/task`, `engine/auth` - must remain unchanged
+- **Infra Layer:** `engine/infra/repo` - factory pattern to select implementation
+- **Vector DB System:** `engine/knowledge/vectordb` - already supports multiple providers
+
+### External Dependencies
+
+**New:**
+- `modernc.org/sqlite` OR `github.com/mattn/go-sqlite3` - SQLite driver
+- Note: `github.com/pressly/goose/v3` already supports SQLite migrations
+
+**Existing (No Changes):**
+- `github.com/jackc/pgx/v5` - PostgreSQL (keep existing)
+- `github.com/Masterminds/squirrel` - Query builder (database-agnostic, reuse)
+
+## Risks & Mitigation
+
+### Risk 1: Production SQLite Misuse
+
+**Risk:** Users deploy SQLite in high-concurrency production scenarios
+
+**Impact:** High - Performance degradation, database locks, poor user experience
+
+**Likelihood:** Medium
+
+**Mitigation:**
+- Clear documentation with decision matrix
+- Startup warnings when SQLite detected in production mode
+- Performance benchmarks in documentation
+- Default remains PostgreSQL
+
+### Risk 2: Vector Search Quality Degradation
+
+**Risk:** External vector DB performs worse than pgvector
+
+**Impact:** Medium - Knowledge base features less effective
+
+**Likelihood:** Low - Qdrant/Redis are mature solutions
+
+**Mitigation:**
+- Document vector DB options with performance characteristics
+- Provide Qdrant as recommended external option
+- Test suite validates all vector DB providers
+- Keep pgvector as recommended for PostgreSQL
+
+### Risk 3: Maintenance Burden
+
+**Risk:** Maintaining two database implementations increases complexity
+
+**Impact:** Medium - More code to maintain, test, debug
+
+**Likelihood:** High
+
+**Mitigation:**
+- Shared test suite for both backends
+- CI/CD matrix testing
+- Clear code organization (separate packages)
+- Comprehensive documentation for contributors
+
+### Risk 4: Feature Divergence
+
+**Risk:** New features only work with PostgreSQL
+
+**Impact:** Low - SQLite users miss features
+
+**Likelihood:** Medium
+
+**Mitigation:**
+- Document database-specific features
+- Feature flags for PostgreSQL-only features
+- Review process checks both implementations
+
+## Implementation Phases
+
+### Phase 1: Foundation (Weeks 1-3)
+
+**Goal:** Core SQLite infrastructure and configuration
+
+**Deliverables:**
+- SQLite driver package (`engine/infra/sqlite`)
+- Configuration support (`database.driver` field)
+- Basic repository implementations (user auth)
+- Migration system for SQLite
+- Test infrastructure setup
+
+**Success Criteria:**
+- `compozy start --db-driver=sqlite --db-path=compozy.db` works
+- User authentication (login, API keys) functional
+- Basic tests passing
+
+### Phase 2: Core Features (Weeks 4-6)
+
+**Goal:** Workflow and task persistence
+
+**Deliverables:**
+- Workflow repository for SQLite
+- Task repository for SQLite (hierarchical support)
+- Transaction handling
+- JSONB β JSON mapping
+- Comprehensive tests
+
+**Success Criteria:**
+- Workflows execute end-to-end with SQLite
+- Task hierarchy (parent/child) works
+- State persistence across restarts
+- Integration tests green
+
+### Phase 3: Production Readiness (Weeks 7-9)
+
+**Goal:** Polish, performance, documentation
+
+**Deliverables:**
+- Performance optimization
+- Concurrent execution support
+- Vector DB validation (SQLite mode)
+- Complete documentation
+- Migration tooling
+
+**Success Criteria:**
+- Performance benchmarks meet targets
+- Documentation complete and reviewed
+- All CI/CD tests passing
+- Community preview feedback positive
+
+### Phase 4: Release (Week 10)
+
+**Goal:** Launch and communication
+
+**Deliverables:**
+- Release announcement
+- Tutorial blog post
+- Video walkthrough
+- Community support plan
+
+**Success Criteria:**
+- Feature released in minor version
+- Documentation published
+- No critical bugs in first week
+
+## Open Questions
+
+1. **SQLite Driver Choice:** `modernc.org/sqlite` (pure Go) vs `github.com/mattn/go-sqlite3` (CGO)?
+ - **Recommendation:** Start with `modernc.org/sqlite` (no CGO, easier cross-compilation)
+ - **Backup:** Fall back to `go-sqlite3` if performance issues
+
+2. **Default Vector DB for SQLite:** Which external vector DB to recommend?
+ - **Options:** Qdrant (best features), Redis (simplest), Filesystem (development)
+ - **Recommendation:** Qdrant for production, Filesystem for development
+
+3. **Concurrency Limits:** Should we enforce max concurrent workflows with SQLite?
+ - **Recommendation:** Document but don't enforce; let users decide
+
+4. **Migration Path:** Support data migration between PostgreSQL β SQLite?
+ - **Recommendation:** Phase 2 feature - export/import JSON tooling
+
+## Appendix
+
+### Related Documents
+
+- **Analysis:** `POSTGRES_ANALYSIS.md` - Comprehensive technical analysis
+- **Architecture:** `.cursor/rules/architecture.mdc` - Project architecture patterns
+- **Configuration:** `.cursor/rules/global-config.mdc` - Config management standards
+
+### Reference Implementation
+
+- **Vector DB Providers:** `engine/knowledge/vectordb/{qdrant,redis,filesystem}.go`
+- **Repository Pattern:** `engine/infra/postgres/*.go`
+- **Migration System:** `engine/infra/postgres/migrations/`
+
+### Version History
+
+| Version | Date | Author | Changes |
+| ------- | ---------- | ----------- | ----------------- |
+| 1.0 | 2025-01-27 | AI Analysis | Initial PRD draft |
diff --git a/tasks/prd-postgres/_task_1.md b/tasks/prd-postgres/_task_1.md
new file mode 100644
index 00000000..1f6e5232
--- /dev/null
+++ b/tasks/prd-postgres/_task_1.md
@@ -0,0 +1,467 @@
+## markdown
+
+## status: pending
+
+
+engine/infra/sqlite + pkg/config
+implementation
+core_feature
+high
+database
+
+
+# Task 1.0: SQLite Foundation Infrastructure
+
+## Overview
+
+Create the foundational SQLite infrastructure including configuration system, connection management, and database migrations. This task establishes the complete SQLite driver implementation with proper schema definitions, enabling all subsequent repository implementations.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md before start
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for test requirements
+- **MANDATORY:** Use `modernc.org/sqlite` (pure Go, no CGO)
+- **MANDATORY:** Enable `PRAGMA foreign_keys = ON` on all connections
+- **MANDATORY:** Separate migration files for SQLite (do not reuse PostgreSQL migrations)
+
+
+
+- Add `driver` field to `DatabaseConfig` with validation (`postgres` | `sqlite`)
+- Implement SQLite `Store` with connection pooling and PRAGMA configuration
+- Port all 4 PostgreSQL migration files to SQLite syntax
+- Support both file-based and in-memory (`:memory:`) databases
+- Health check implementation for SQLite
+- Migration runner without advisory locks (SQLite doesn't support them)
+
+
+## Subtasks
+
+- [ ] 1.1 Add database driver configuration to `pkg/config/config.go`
+- [ ] 1.2 Create `engine/infra/sqlite/` package structure
+- [ ] 1.3 Implement `Store` with connection management
+- [ ] 1.4 Port migration: `create_workflow_states.sql`
+- [ ] 1.5 Port migration: `create_task_states.sql`
+- [ ] 1.6 Port migration: `create_users.sql`
+- [ ] 1.7 Port migration: `create_api_keys.sql`
+- [ ] 1.8 Implement migration runner (`migrations.go`)
+- [ ] 1.9 Write unit tests for config validation
+- [ ] 1.10 Write unit tests for Store operations
+- [ ] 1.11 Write integration tests for migrations
+
+## Implementation Details
+
+### 1.1 Database Configuration (`pkg/config/config.go`)
+
+**Add to `DatabaseConfig` struct:**
+
+```go
+type DatabaseConfig struct {
+ // Driver selection
+ Driver string `koanf:"driver" json:"driver" yaml:"driver" env:"DB_DRIVER" validate:"oneof=postgres sqlite"`
+
+ // PostgreSQL-specific (existing fields, unchanged)
+ ConnString string `koanf:"conn_string" json:"conn_string" yaml:"conn_string"`
+ Host string `koanf:"host" json:"host" yaml:"host"`
+ Port string `koanf:"port" json:"port" yaml:"port"`
+ User string `koanf:"user" json:"user" yaml:"user"`
+ Password string `koanf:"password" json:"password" yaml:"password"`
+ DBName string `koanf:"dbname" json:"dbname" yaml:"dbname"`
+ SSLMode string `koanf:"sslmode" json:"sslmode" yaml:"sslmode"`
+
+ // SQLite-specific (new)
+ Path string `koanf:"path" json:"path" yaml:"path"` // File path or ":memory:"
+
+ // Common settings (existing)
+ MaxOpenConns int `koanf:"max_open_conns" json:"max_open_conns" yaml:"max_open_conns"`
+ MaxIdleConns int `koanf:"max_idle_conns" json:"max_idle_conns" yaml:"max_idle_conns"`
+ // ... rest unchanged
+}
+
+// Add validation method
+func (c *DatabaseConfig) Validate() error {
+ // Default to postgres if not specified
+ if c.Driver == "" {
+ c.Driver = "postgres"
+ }
+
+ switch c.Driver {
+ case "postgres":
+ // Validate PostgreSQL-specific fields
+ if c.Host == "" && c.ConnString == "" {
+ return fmt.Errorf("postgres driver requires host or conn_string")
+ }
+ case "sqlite":
+ // Validate SQLite-specific fields
+ if c.Path == "" {
+ return fmt.Errorf("sqlite driver requires path")
+ }
+ default:
+ return fmt.Errorf("unsupported database driver: %s", c.Driver)
+ }
+
+ return nil
+}
+```
+
+### 1.2-1.3 SQLite Store (`engine/infra/sqlite/store.go`)
+
+**Reference:** `engine/infra/postgres/store.go` for patterns
+
+```go
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "time"
+
+ _ "modernc.org/sqlite" // Pure Go SQLite driver
+)
+
+type Config struct {
+ Path string
+ MaxOpenConns int
+ MaxIdleConns int
+}
+
+type Store struct {
+ db *sql.DB
+ path string
+}
+
+func NewStore(ctx context.Context, cfg *Config) (*Store, error) {
+ // Apply defaults
+ if cfg.MaxOpenConns == 0 {
+ cfg.MaxOpenConns = 25
+ }
+ if cfg.MaxIdleConns == 0 {
+ cfg.MaxIdleConns = 5
+ }
+
+ // Open database
+ db, err := sql.Open("sqlite", cfg.Path)
+ if err != nil {
+ return nil, fmt.Errorf("open sqlite database: %w", err)
+ }
+
+ // Configure connection pool
+ db.SetMaxOpenConns(cfg.MaxOpenConns)
+ db.SetMaxIdleConns(cfg.MaxIdleConns)
+ db.SetConnMaxLifetime(time.Hour)
+
+ // Enable foreign keys (CRITICAL for SQLite)
+ if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
+ db.Close()
+ return nil, fmt.Errorf("enable foreign keys: %w", err)
+ }
+
+ // Enable WAL mode for better concurrency
+ if _, err := db.ExecContext(ctx, "PRAGMA journal_mode = WAL"); err != nil {
+ db.Close()
+ return nil, fmt.Errorf("enable WAL mode: %w", err)
+ }
+
+ store := &Store{
+ db: db,
+ path: cfg.Path,
+ }
+
+ return store, nil
+}
+
+func (s *Store) DB() *sql.DB {
+ return s.db
+}
+
+func (s *Store) Close(ctx context.Context) error {
+ if s.db != nil {
+ return s.db.Close()
+ }
+ return nil
+}
+
+func (s *Store) HealthCheck(ctx context.Context) error {
+ // Check foreign keys are enabled
+ var fkEnabled int
+ if err := s.db.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&fkEnabled); err != nil {
+ return fmt.Errorf("health check failed: %w", err)
+ }
+ if fkEnabled != 1 {
+ return fmt.Errorf("foreign keys not enabled")
+ }
+
+ // Simple ping
+ if err := s.db.PingContext(ctx); err != nil {
+ return fmt.Errorf("ping failed: %w", err)
+ }
+
+ return nil
+}
+```
+
+### 1.4-1.7 Migration Files
+
+**Create:** `engine/infra/sqlite/migrations/`
+
+**20250603124835_create_workflow_states.sql:**
+```sql
+-- +goose Up
+-- +goose StatementBegin
+CREATE TABLE IF NOT EXISTS workflow_states (
+ workflow_exec_id TEXT NOT NULL PRIMARY KEY,
+ workflow_id TEXT NOT NULL,
+ status TEXT NOT NULL,
+ usage TEXT, -- JSON as TEXT
+ input TEXT, -- JSON as TEXT
+ output TEXT, -- JSON as TEXT
+ error TEXT, -- JSON as TEXT
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+
+ CHECK (usage IS NULL OR json_type(usage) = 'array')
+);
+
+CREATE INDEX IF NOT EXISTS idx_workflow_states_status ON workflow_states (status);
+CREATE INDEX IF NOT EXISTS idx_workflow_states_workflow_id ON workflow_states (workflow_id);
+CREATE INDEX IF NOT EXISTS idx_workflow_states_workflow_status ON workflow_states (workflow_id, status);
+CREATE INDEX IF NOT EXISTS idx_workflow_states_created_at ON workflow_states (created_at);
+CREATE INDEX IF NOT EXISTS idx_workflow_states_updated_at ON workflow_states (updated_at);
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE IF EXISTS workflow_states;
+-- +goose StatementEnd
+```
+
+**20250603124915_create_task_states.sql:**
+```sql
+-- +goose Up
+-- +goose StatementBegin
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE IF NOT EXISTS task_states (
+ component TEXT NOT NULL,
+ status TEXT NOT NULL,
+ task_exec_id TEXT NOT NULL PRIMARY KEY,
+ task_id TEXT NOT NULL,
+ workflow_exec_id TEXT NOT NULL,
+ workflow_id TEXT NOT NULL,
+ execution_type TEXT NOT NULL DEFAULT 'basic',
+ usage TEXT, -- JSON as TEXT
+ agent_id TEXT,
+ tool_id TEXT,
+ action_id TEXT,
+ parent_state_id TEXT,
+ input TEXT, -- JSON as TEXT
+ output TEXT, -- JSON as TEXT
+ error TEXT, -- JSON as TEXT
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
+
+ FOREIGN KEY (workflow_exec_id)
+ REFERENCES workflow_states (workflow_exec_id)
+ ON DELETE CASCADE,
+
+ FOREIGN KEY (parent_state_id)
+ REFERENCES task_states (task_exec_id)
+ ON DELETE CASCADE,
+
+ CHECK (
+ (execution_type = 'basic' AND (
+ (agent_id IS NOT NULL AND action_id IS NOT NULL AND tool_id IS NULL) OR
+ (tool_id IS NOT NULL AND agent_id IS NULL AND action_id IS NULL) OR
+ (agent_id IS NULL AND action_id IS NULL AND tool_id IS NULL)
+ )) OR
+ (execution_type = 'router' AND agent_id IS NULL AND action_id IS NULL AND tool_id IS NULL) OR
+ (execution_type IN ('parallel', 'collection', 'composite')) OR
+ (execution_type NOT IN ('parallel', 'collection', 'composite', 'basic', 'router'))
+ ),
+
+ CHECK (usage IS NULL OR json_type(usage) = 'array')
+);
+
+CREATE INDEX IF NOT EXISTS idx_task_states_status ON task_states (status);
+CREATE INDEX IF NOT EXISTS idx_task_states_workflow_id ON task_states (workflow_id);
+CREATE INDEX IF NOT EXISTS idx_task_states_workflow_exec_id ON task_states (workflow_exec_id);
+CREATE INDEX IF NOT EXISTS idx_task_states_task_id ON task_states (task_id);
+CREATE INDEX IF NOT EXISTS idx_task_states_parent_state_id ON task_states (parent_state_id);
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE IF EXISTS task_states;
+-- +goose StatementEnd
+```
+
+**20250711163857_create_users.sql:**
+```sql
+-- +goose Up
+-- +goose StatementBegin
+CREATE TABLE IF NOT EXISTS users (
+ id TEXT NOT NULL PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_ci ON users (lower(email));
+CREATE INDEX IF NOT EXISTS idx_users_role ON users (role);
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE IF EXISTS users;
+-- +goose StatementEnd
+```
+
+**20250711163858_create_api_keys.sql:**
+```sql
+-- +goose Up
+-- +goose StatementBegin
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE IF NOT EXISTS api_keys (
+ id TEXT NOT NULL PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ hash BLOB NOT NULL,
+ prefix TEXT NOT NULL UNIQUE,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
+ last_used TEXT,
+
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys (hash);
+CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys (user_id);
+CREATE INDEX IF NOT EXISTS idx_api_keys_created_at ON api_keys (created_at);
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE IF EXISTS api_keys;
+-- +goose StatementEnd
+```
+
+### 1.8 Migration Runner (`engine/infra/sqlite/migrations.go`)
+
+```go
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "embed"
+ "fmt"
+
+ "github.com/pressly/goose/v3"
+ _ "modernc.org/sqlite"
+)
+
+//go:embed migrations/*.sql
+var migrationsFS embed.FS
+
+func ApplyMigrations(ctx context.Context, dbPath string) error {
+ db, err := sql.Open("sqlite", dbPath)
+ if err != nil {
+ return fmt.Errorf("open db for migrations: %w", err)
+ }
+ defer db.Close()
+
+ // Enable foreign keys
+ if _, err := db.ExecContext(ctx, "PRAGMA foreign_keys = ON"); err != nil {
+ return fmt.Errorf("enable foreign keys: %w", err)
+ }
+
+ goose.SetBaseFS(migrationsFS)
+ if err := goose.SetDialect("sqlite3"); err != nil {
+ return fmt.Errorf("set dialect: %w", err)
+ }
+
+ if err := goose.UpContext(ctx, db, "migrations"); err != nil {
+ return fmt.Errorf("apply migrations: %w", err)
+ }
+
+ return nil
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `engine/infra/sqlite/store.go`
+- `engine/infra/sqlite/migrations.go`
+- `engine/infra/sqlite/config.go`
+- `engine/infra/sqlite/doc.go`
+- `engine/infra/sqlite/migrations/20250603124835_create_workflow_states.sql`
+- `engine/infra/sqlite/migrations/20250603124915_create_task_states.sql`
+- `engine/infra/sqlite/migrations/20250711163857_create_users.sql`
+- `engine/infra/sqlite/migrations/20250711163858_create_api_keys.sql`
+
+**Modified Files:**
+- `pkg/config/config.go` - Add `Driver` and `Path` fields
+
+### Dependent Files
+
+- `engine/infra/postgres/store.go` - Reference for patterns
+- `engine/infra/postgres/migrations/*.sql` - Source for porting
+
+## Deliverables
+
+- [ ] `pkg/config/config.go` updated with `driver` field and validation
+- [ ] `engine/infra/sqlite/` package created with proper structure
+- [ ] `engine/infra/sqlite/store.go` with connection management
+- [ ] `engine/infra/sqlite/migrations.go` with migration runner
+- [ ] 4 migration files ported from PostgreSQL to SQLite syntax
+- [ ] All unit tests passing for config validation
+- [ ] All unit tests passing for Store operations
+- [ ] All integration tests passing for migrations
+- [ ] Documentation in `engine/infra/sqlite/doc.go`
+
+## Tests
+
+### Unit Tests: Configuration (`pkg/config/config_test.go`)
+
+- [ ] `TestDatabaseConfig/Should_default_to_postgres_when_driver_empty`
+- [ ] `TestDatabaseConfig/Should_accept_postgres_driver_explicitly`
+- [ ] `TestDatabaseConfig/Should_accept_sqlite_driver`
+- [ ] `TestDatabaseConfig/Should_reject_invalid_driver`
+- [ ] `TestDatabaseConfig/Should_require_path_for_sqlite`
+- [ ] `TestDatabaseConfig/Should_require_connection_params_for_postgres`
+- [ ] `TestDatabaseConfig/Should_validate_sqlite_path_format`
+
+### Unit Tests: Store (`engine/infra/sqlite/store_test.go`)
+
+- [ ] `TestStore/Should_create_file_database_at_specified_path`
+- [ ] `TestStore/Should_create_in_memory_database_for_memory_path`
+- [ ] `TestStore/Should_enable_foreign_keys_on_connection`
+- [ ] `TestStore/Should_handle_concurrent_connections`
+- [ ] `TestStore/Should_return_error_for_invalid_path`
+- [ ] `TestStore/Should_close_cleanly`
+- [ ] `TestStore/Should_perform_health_check_successfully`
+
+### Integration Tests: Migrations (`engine/infra/sqlite/migrations_test.go`)
+
+- [ ] `TestMigrations/Should_apply_all_migrations_successfully`
+- [ ] `TestMigrations/Should_create_all_required_tables`
+- [ ] `TestMigrations/Should_create_all_indexes`
+- [ ] `TestMigrations/Should_enforce_foreign_keys`
+- [ ] `TestMigrations/Should_enforce_check_constraints`
+- [ ] `TestMigrations/Should_rollback_migrations`
+- [ ] `TestMigrations/Should_be_idempotent`
+
+## Success Criteria
+
+- [ ] Configuration validates driver correctly (postgres/sqlite)
+- [ ] SQLite store creates file-based and in-memory databases
+- [ ] Foreign keys are enabled on all connections
+- [ ] WAL mode is enabled for better concurrency
+- [ ] All 4 tables created with correct schema
+- [ ] All indexes created correctly
+- [ ] Foreign key constraints enforced
+- [ ] Check constraints enforced (role, execution_type, JSON type)
+- [ ] Migrations are idempotent (can run multiple times)
+- [ ] Health check validates database state
+- [ ] All tests pass: `go test ./pkg/config/... ./engine/infra/sqlite/...`
+- [ ] Code passes linting: `golangci-lint run ./pkg/config/... ./engine/infra/sqlite/...`
diff --git a/tasks/prd-postgres/_task_2.md b/tasks/prd-postgres/_task_2.md
new file mode 100644
index 00000000..7d5a1fb0
--- /dev/null
+++ b/tasks/prd-postgres/_task_2.md
@@ -0,0 +1,358 @@
+## markdown
+
+## status: pending
+
+
+engine/infra/sqlite
+implementation
+core_feature
+medium
+database
+
+
+# Task 2.0: Authentication Repository (SQLite)
+
+## Overview
+
+Implement SQLite-backed authentication repository for user and API key management. Port the PostgreSQL `authrepo.go` implementation to SQLite, handling user CRUD operations, API key management, and foreign key constraints.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md section on Auth Repository
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for test requirements
+- **DEPENDENCY:** Requires Task 1.0 (Foundation) complete
+- **MANDATORY:** Use `database/sql` standard library (no pgx)
+- **MANDATORY:** Case-insensitive email queries using `lower(email)`
+- **MANDATORY:** Foreign key constraints enforced for API keys
+
+
+
+- Implement `AuthRepo` struct using `*sql.DB`
+- Port all methods from `engine/infra/postgres/authrepo.go`
+- User CRUD: Create, GetByID, GetByEmail, List, Update, Delete
+- API key operations: Create, GetByHash, GetByPrefix, Delete
+- Use `?` placeholders (not `$1`)
+- Handle TEXT timestamps (ISO8601 format)
+- Enforce foreign key CASCADE on API keys
+
+
+## Subtasks
+
+- [ ] 2.1 Create `engine/infra/sqlite/authrepo.go` structure
+- [ ] 2.2 Implement user CRUD operations
+- [ ] 2.3 Implement API key operations
+- [ ] 2.4 Add helper functions for common queries
+- [ ] 2.5 Write unit tests for user operations
+- [ ] 2.6 Write unit tests for API key operations
+- [ ] 2.7 Write integration tests for cascade deletes
+
+## Implementation Details
+
+### Reference Implementation
+
+**Source:** `engine/infra/postgres/authrepo.go`
+
+### 2.1 Repository Structure
+
+```go
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/compozy/compozy/engine/auth/uc"
+ "github.com/compozy/compozy/engine/core"
+ "github.com/compozy/compozy/engine/auth/model"
+)
+
+type AuthRepo struct {
+ db *sql.DB
+}
+
+func NewAuthRepo(db *sql.DB) uc.Repository {
+ return &AuthRepo{db: db}
+}
+```
+
+### 2.2 User Operations
+
+```go
+func (r *AuthRepo) CreateUser(ctx context.Context, user *model.User) error {
+ query := `
+ INSERT INTO users (id, email, role, created_at)
+ VALUES (?, ?, ?, ?)
+ `
+
+ _, err := r.db.ExecContext(ctx, query,
+ user.ID.String(),
+ user.Email,
+ user.Role,
+ user.CreatedAt.Format(time.RFC3339),
+ )
+ if err != nil {
+ return fmt.Errorf("create user: %w", err)
+ }
+
+ return nil
+}
+
+func (r *AuthRepo) GetUserByID(ctx context.Context, id core.ID) (*model.User, error) {
+ query := `
+ SELECT id, email, role, created_at
+ FROM users
+ WHERE id = ?
+ `
+
+ var user model.User
+ var createdAt string
+
+ err := r.db.QueryRowContext(ctx, query, id.String()).Scan(
+ &user.ID,
+ &user.Email,
+ &user.Role,
+ &createdAt,
+ )
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("user not found: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get user by id: %w", err)
+ }
+
+ // Parse timestamp
+ user.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+
+ return &user, nil
+}
+
+func (r *AuthRepo) GetUserByEmail(ctx context.Context, email string) (*model.User, error) {
+ query := `
+ SELECT id, email, role, created_at
+ FROM users
+ WHERE lower(email) = lower(?)
+ `
+
+ var user model.User
+ var createdAt string
+
+ err := r.db.QueryRowContext(ctx, query, email).Scan(
+ &user.ID,
+ &user.Email,
+ &user.Role,
+ &createdAt,
+ )
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("user not found: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get user by email: %w", err)
+ }
+
+ user.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+
+ return &user, nil
+}
+
+func (r *AuthRepo) ListUsers(ctx context.Context) ([]*model.User, error) {
+ query := `
+ SELECT id, email, role, created_at
+ FROM users
+ ORDER BY created_at DESC
+ `
+
+ rows, err := r.db.QueryContext(ctx, query)
+ if err != nil {
+ return nil, fmt.Errorf("list users: %w", err)
+ }
+ defer rows.Close()
+
+ var users []*model.User
+ for rows.Next() {
+ var user model.User
+ var createdAt string
+
+ if err := rows.Scan(&user.ID, &user.Email, &user.Role, &createdAt); err != nil {
+ return nil, fmt.Errorf("scan user: %w", err)
+ }
+
+ user.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+ users = append(users, &user)
+ }
+
+ return users, rows.Err()
+}
+
+func (r *AuthRepo) DeleteUser(ctx context.Context, id core.ID) error {
+ query := `DELETE FROM users WHERE id = ?`
+
+ result, err := r.db.ExecContext(ctx, query, id.String())
+ if err != nil {
+ return fmt.Errorf("delete user: %w", err)
+ }
+
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return fmt.Errorf("user not found: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+```
+
+### 2.3 API Key Operations
+
+```go
+func (r *AuthRepo) CreateAPIKey(ctx context.Context, key *model.APIKey) error {
+ query := `
+ INSERT INTO api_keys (id, user_id, hash, prefix, created_at)
+ VALUES (?, ?, ?, ?, ?)
+ `
+
+ _, err := r.db.ExecContext(ctx, query,
+ key.ID.String(),
+ key.UserID.String(),
+ key.Hash,
+ key.Prefix,
+ key.CreatedAt.Format(time.RFC3339),
+ )
+ if err != nil {
+ return fmt.Errorf("create api key: %w", err)
+ }
+
+ return nil
+}
+
+func (r *AuthRepo) GetAPIKeyByHash(ctx context.Context, hash []byte) (*model.APIKey, error) {
+ query := `
+ SELECT id, user_id, hash, prefix, created_at, last_used
+ FROM api_keys
+ WHERE hash = ?
+ `
+
+ var key model.APIKey
+ var createdAt, lastUsed sql.NullString
+
+ err := r.db.QueryRowContext(ctx, query, hash).Scan(
+ &key.ID,
+ &key.UserID,
+ &key.Hash,
+ &key.Prefix,
+ &createdAt,
+ &lastUsed,
+ )
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("api key not found: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get api key by hash: %w", err)
+ }
+
+ key.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
+ if lastUsed.Valid {
+ key.LastUsed, _ = time.Parse(time.RFC3339, lastUsed.String)
+ }
+
+ return &key, nil
+}
+
+func (r *AuthRepo) UpdateAPIKeyLastUsed(ctx context.Context, id core.ID) error {
+ query := `
+ UPDATE api_keys
+ SET last_used = ?
+ WHERE id = ?
+ `
+
+ _, err := r.db.ExecContext(ctx, query,
+ time.Now().Format(time.RFC3339),
+ id.String(),
+ )
+ if err != nil {
+ return fmt.Errorf("update api key last used: %w", err)
+ }
+
+ return nil
+}
+
+func (r *AuthRepo) DeleteAPIKey(ctx context.Context, id core.ID) error {
+ query := `DELETE FROM api_keys WHERE id = ?`
+
+ result, err := r.db.ExecContext(ctx, query, id.String())
+ if err != nil {
+ return fmt.Errorf("delete api key: %w", err)
+ }
+
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return fmt.Errorf("api key not found: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `engine/infra/sqlite/authrepo.go`
+- `engine/infra/sqlite/authrepo_test.go`
+
+**Reference Files:**
+- `engine/infra/postgres/authrepo.go` - Source implementation
+- `engine/auth/uc/repository.go` - Interface definition
+- `engine/auth/model/user.go` - User model
+- `engine/auth/model/api_key.go` - API key model
+
+### Dependent Files
+
+- `engine/infra/sqlite/store.go` - Database connection (from Task 1.0)
+- `engine/infra/sqlite/migrations/*.sql` - Schema (from Task 1.0)
+
+## Deliverables
+
+- [ ] `engine/infra/sqlite/authrepo.go` with complete implementation
+- [ ] All user CRUD operations working
+- [ ] All API key operations working
+- [ ] Case-insensitive email queries implemented
+- [ ] Foreign key constraints respected
+- [ ] All unit tests passing
+- [ ] All integration tests passing
+- [ ] Code passes linting
+
+## Tests
+
+### Unit Tests (`engine/infra/sqlite/authrepo_test.go`)
+
+- [ ] `TestAuthRepo/Should_create_user_successfully`
+- [ ] `TestAuthRepo/Should_get_user_by_id`
+- [ ] `TestAuthRepo/Should_get_user_by_email_case_insensitive`
+- [ ] `TestAuthRepo/Should_return_error_for_duplicate_email`
+- [ ] `TestAuthRepo/Should_list_all_users`
+- [ ] `TestAuthRepo/Should_delete_user`
+- [ ] `TestAuthRepo/Should_create_api_key`
+- [ ] `TestAuthRepo/Should_get_api_key_by_hash`
+- [ ] `TestAuthRepo/Should_update_api_key_last_used`
+- [ ] `TestAuthRepo/Should_delete_api_key`
+- [ ] `TestAuthRepo/Should_cascade_delete_api_keys_when_user_deleted`
+- [ ] `TestAuthRepo/Should_enforce_foreign_key_constraint`
+
+### Edge Cases
+
+- [ ] `TestAuthRepo/Should_handle_missing_user_gracefully`
+- [ ] `TestAuthRepo/Should_handle_missing_api_key_gracefully`
+- [ ] `TestAuthRepo/Should_reject_invalid_foreign_key`
+- [ ] `TestAuthRepo/Should_handle_null_last_used_timestamp`
+
+## Success Criteria
+
+- [ ] All user CRUD operations work correctly
+- [ ] Email queries are case-insensitive
+- [ ] API keys correctly reference users via foreign key
+- [ ] Cascade delete removes API keys when user deleted
+- [ ] Foreign key violations rejected appropriately
+- [ ] Timestamps stored and retrieved correctly (ISO8601 format)
+- [ ] All tests pass: `go test ./engine/infra/sqlite/authrepo_test.go`
+- [ ] Code passes linting: `golangci-lint run ./engine/infra/sqlite/authrepo.go`
+- [ ] Repository implements `uc.Repository` interface correctly
diff --git a/tasks/prd-postgres/_task_3.md b/tasks/prd-postgres/_task_3.md
new file mode 100644
index 00000000..fdf3dad6
--- /dev/null
+++ b/tasks/prd-postgres/_task_3.md
@@ -0,0 +1,403 @@
+## markdown
+
+## status: pending
+
+
+engine/infra/sqlite
+implementation
+core_feature
+medium
+database
+
+
+# Task 3.0: Workflow Repository (SQLite)
+
+## Overview
+
+Implement SQLite-backed workflow state repository for workflow execution persistence. Port the PostgreSQL `workflowrepo.go` implementation to SQLite, handling JSONB β JSON TEXT conversion, workflow state management, and transaction support.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md section on Workflow Repository
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for test requirements
+- **DEPENDENCY:** Requires Task 1.0 (Foundation) complete
+- **MANDATORY:** Convert JSONB to JSON TEXT for SQLite
+- **MANDATORY:** Use `json.Marshal`/`json.Unmarshal` for JSON fields
+- **MANDATORY:** Handle NULL JSON fields correctly
+- **MANDATORY:** Use transactions for atomic operations
+
+
+
+- Implement `WorkflowRepo` struct using `*sql.DB`
+- Port all methods from `engine/infra/postgres/workflowrepo.go`
+- CRUD operations: UpsertState, GetState, ListStates, UpdateStatus
+- Handle JSON fields: usage, input, output, error
+- Support filtering by status and workflow_id
+- Transaction support for atomic updates
+- Use `?` placeholders (not `$1`)
+- Handle TEXT timestamps (ISO8601 format)
+
+
+## Subtasks
+
+- [ ] 3.1 Create `engine/infra/sqlite/workflowrepo.go` structure
+- [ ] 3.2 Implement workflow state upsert
+- [ ] 3.3 Implement workflow state retrieval
+- [ ] 3.4 Implement list with filtering
+- [ ] 3.5 Implement status updates
+- [ ] 3.6 Add JSON marshaling helpers
+- [ ] 3.7 Write unit tests for CRUD operations
+- [ ] 3.8 Write unit tests for JSON handling
+- [ ] 3.9 Write integration tests for transactions
+
+## Implementation Details
+
+### Reference Implementation
+
+**Source:** `engine/infra/postgres/workflowrepo.go`
+
+### 3.1 Repository Structure
+
+```go
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/compozy/compozy/engine/core"
+ "github.com/compozy/compozy/engine/workflow"
+)
+
+type WorkflowRepo struct {
+ db *sql.DB
+}
+
+func NewWorkflowRepo(db *sql.DB) workflow.Repository {
+ return &WorkflowRepo{db: db}
+}
+```
+
+### 3.2 Upsert State
+
+```go
+func (r *WorkflowRepo) UpsertState(ctx context.Context, state *workflow.State) error {
+ // Marshal JSON fields
+ usageJSON, err := marshalJSON(state.Usage)
+ if err != nil {
+ return fmt.Errorf("marshal usage: %w", err)
+ }
+
+ inputJSON, err := marshalJSON(state.Input)
+ if err != nil {
+ return fmt.Errorf("marshal input: %w", err)
+ }
+
+ outputJSON, err := marshalJSON(state.Output)
+ if err != nil {
+ return fmt.Errorf("marshal output: %w", err)
+ }
+
+ errorJSON, err := marshalJSON(state.Error)
+ if err != nil {
+ return fmt.Errorf("marshal error: %w", err)
+ }
+
+ query := `
+ INSERT INTO workflow_states (
+ workflow_exec_id, workflow_id, status,
+ usage, input, output, error,
+ created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT (workflow_exec_id) DO UPDATE SET
+ workflow_id = excluded.workflow_id,
+ status = excluded.status,
+ usage = excluded.usage,
+ input = excluded.input,
+ output = excluded.output,
+ error = excluded.error,
+ updated_at = excluded.updated_at
+ `
+
+ now := time.Now().Format(time.RFC3339)
+
+ _, err = r.db.ExecContext(ctx, query,
+ state.WorkflowExecID.String(),
+ state.WorkflowID,
+ state.Status,
+ usageJSON,
+ inputJSON,
+ outputJSON,
+ errorJSON,
+ state.CreatedAt.Format(time.RFC3339),
+ now,
+ )
+ if err != nil {
+ return fmt.Errorf("upsert workflow state: %w", err)
+ }
+
+ return nil
+}
+```
+
+### 3.3 Get State
+
+```go
+func (r *WorkflowRepo) GetState(ctx context.Context, workflowExecID core.ID) (*workflow.State, error) {
+ query := `
+ SELECT workflow_exec_id, workflow_id, status,
+ usage, input, output, error,
+ created_at, updated_at
+ FROM workflow_states
+ WHERE workflow_exec_id = ?
+ `
+
+ var state workflow.State
+ var usageJSON, inputJSON, outputJSON, errorJSON sql.NullString
+ var createdAt, updatedAt string
+
+ err := r.db.QueryRowContext(ctx, query, workflowExecID.String()).Scan(
+ &state.WorkflowExecID,
+ &state.WorkflowID,
+ &state.Status,
+ &usageJSON,
+ &inputJSON,
+ &outputJSON,
+ &errorJSON,
+ &createdAt,
+ &updatedAt,
+ )
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("workflow state not found: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get workflow state: %w", err)
+ }
+
+ // Unmarshal JSON fields
+ if err := unmarshalJSON(usageJSON, &state.Usage); err != nil {
+ return nil, fmt.Errorf("unmarshal usage: %w", err)
+ }
+
+ if err := unmarshalJSON(inputJSON, &state.Input); err != nil {
+ return nil, fmt.Errorf("unmarshal input: %w", err)
+ }
+
+ if err := unmarshalJSON(outputJSON, &state.Output); err != nil {
+ return nil, fmt.Errorf("unmarshal output: %w", err)
+ }
+
+ if err := unmarshalJSON(errorJSON, &state.Error); err != nil {
+ return nil, fmt.Errorf("unmarshal error: %w", err)
+ }
+
+ // Parse timestamps
+ state.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+ state.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
+
+ return &state, nil
+}
+```
+
+### 3.4 List States
+
+```go
+func (r *WorkflowRepo) ListStates(ctx context.Context, filter workflow.Filter) ([]*workflow.State, error) {
+ query := `
+ SELECT workflow_exec_id, workflow_id, status,
+ usage, input, output, error,
+ created_at, updated_at
+ FROM workflow_states
+ WHERE 1=1
+ `
+
+ args := []any{}
+
+ // Apply filters
+ if filter.WorkflowID != "" {
+ query += " AND workflow_id = ?"
+ args = append(args, filter.WorkflowID)
+ }
+
+ if filter.Status != "" {
+ query += " AND status = ?"
+ args = append(args, filter.Status)
+ }
+
+ query += " ORDER BY created_at DESC"
+
+ if filter.Limit > 0 {
+ query += " LIMIT ?"
+ args = append(args, filter.Limit)
+ }
+
+ rows, err := r.db.QueryContext(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("list workflow states: %w", err)
+ }
+ defer rows.Close()
+
+ var states []*workflow.State
+ for rows.Next() {
+ var state workflow.State
+ var usageJSON, inputJSON, outputJSON, errorJSON sql.NullString
+ var createdAt, updatedAt string
+
+ if err := rows.Scan(
+ &state.WorkflowExecID,
+ &state.WorkflowID,
+ &state.Status,
+ &usageJSON,
+ &inputJSON,
+ &outputJSON,
+ &errorJSON,
+ &createdAt,
+ &updatedAt,
+ ); err != nil {
+ return nil, fmt.Errorf("scan workflow state: %w", err)
+ }
+
+ // Unmarshal JSON fields
+ unmarshalJSON(usageJSON, &state.Usage)
+ unmarshalJSON(inputJSON, &state.Input)
+ unmarshalJSON(outputJSON, &state.Output)
+ unmarshalJSON(errorJSON, &state.Error)
+
+ state.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+ state.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
+
+ states = append(states, &state)
+ }
+
+ return states, rows.Err()
+}
+```
+
+### 3.5 Update Status
+
+```go
+func (r *WorkflowRepo) UpdateStatus(ctx context.Context, workflowExecID core.ID, status string) error {
+ query := `
+ UPDATE workflow_states
+ SET status = ?, updated_at = ?
+ WHERE workflow_exec_id = ?
+ `
+
+ result, err := r.db.ExecContext(ctx, query,
+ status,
+ time.Now().Format(time.RFC3339),
+ workflowExecID.String(),
+ )
+ if err != nil {
+ return fmt.Errorf("update workflow status: %w", err)
+ }
+
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return fmt.Errorf("workflow state not found: %w", core.ErrNotFound)
+ }
+
+ return nil
+}
+```
+
+### 3.6 JSON Helpers
+
+```go
+// marshalJSON converts a value to JSON string (or NULL)
+func marshalJSON(v any) (sql.NullString, error) {
+ if v == nil {
+ return sql.NullString{Valid: false}, nil
+ }
+
+ data, err := json.Marshal(v)
+ if err != nil {
+ return sql.NullString{}, err
+ }
+
+ return sql.NullString{String: string(data), Valid: true}, nil
+}
+
+// unmarshalJSON parses JSON string into value (handles NULL)
+func unmarshalJSON(ns sql.NullString, v any) error {
+ if !ns.Valid || ns.String == "" {
+ return nil
+ }
+
+ return json.Unmarshal([]byte(ns.String), v)
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `engine/infra/sqlite/workflowrepo.go`
+- `engine/infra/sqlite/workflowrepo_test.go`
+- `engine/infra/sqlite/json_helpers.go` (optional, for shared JSON functions)
+
+**Reference Files:**
+- `engine/infra/postgres/workflowrepo.go` - Source implementation
+- `engine/workflow/repository.go` - Interface definition
+- `engine/workflow/state.go` - Workflow state model
+
+### Dependent Files
+
+- `engine/infra/sqlite/store.go` - Database connection (from Task 1.0)
+- `engine/infra/sqlite/migrations/*.sql` - Schema (from Task 1.0)
+
+## Deliverables
+
+- [ ] `engine/infra/sqlite/workflowrepo.go` with complete implementation
+- [ ] All workflow CRUD operations working
+- [ ] JSON fields (usage, input, output, error) handled correctly
+- [ ] Filtering by status and workflow_id working
+- [ ] Transaction support implemented
+- [ ] All unit tests passing
+- [ ] All integration tests passing
+- [ ] Code passes linting
+
+## Tests
+
+### Unit Tests (`engine/infra/sqlite/workflowrepo_test.go`)
+
+- [ ] `TestWorkflowRepo/Should_upsert_workflow_state`
+- [ ] `TestWorkflowRepo/Should_get_workflow_state_by_exec_id`
+- [ ] `TestWorkflowRepo/Should_list_workflows_by_status`
+- [ ] `TestWorkflowRepo/Should_list_workflows_by_workflow_id`
+- [ ] `TestWorkflowRepo/Should_update_workflow_status`
+- [ ] `TestWorkflowRepo/Should_complete_workflow_with_output`
+- [ ] `TestWorkflowRepo/Should_handle_jsonb_usage_field`
+- [ ] `TestWorkflowRepo/Should_handle_jsonb_input_field`
+- [ ] `TestWorkflowRepo/Should_handle_jsonb_output_field`
+- [ ] `TestWorkflowRepo/Should_handle_jsonb_error_field`
+- [ ] `TestWorkflowRepo/Should_handle_null_json_fields`
+- [ ] `TestWorkflowRepo/Should_merge_usage_statistics`
+
+### Integration Tests
+
+- [ ] `TestWorkflowRepo/Should_execute_transaction_atomically`
+- [ ] `TestWorkflowRepo/Should_rollback_on_error`
+- [ ] `TestWorkflowRepo/Should_handle_concurrent_updates`
+
+### Edge Cases
+
+- [ ] `TestWorkflowRepo/Should_handle_missing_workflow_gracefully`
+- [ ] `TestWorkflowRepo/Should_handle_empty_json_arrays`
+- [ ] `TestWorkflowRepo/Should_handle_complex_nested_json`
+- [ ] `TestWorkflowRepo/Should_validate_json_type_constraint`
+
+## Success Criteria
+
+- [ ] All workflow CRUD operations work correctly
+- [ ] JSONB fields marshaled/unmarshaled correctly (usage, input, output, error)
+- [ ] NULL JSON fields handled properly
+- [ ] Filtering by status and workflow_id works
+- [ ] Timestamps stored and retrieved correctly (ISO8601 format)
+- [ ] Upsert logic (INSERT ... ON CONFLICT) works correctly
+- [ ] Transactions commit and rollback correctly
+- [ ] All tests pass: `go test ./engine/infra/sqlite/workflowrepo_test.go`
+- [ ] Code passes linting: `golangci-lint run ./engine/infra/sqlite/workflowrepo.go`
+- [ ] Repository implements `workflow.Repository` interface correctly
diff --git a/tasks/prd-postgres/_task_4.md b/tasks/prd-postgres/_task_4.md
new file mode 100644
index 00000000..bd15c681
--- /dev/null
+++ b/tasks/prd-postgres/_task_4.md
@@ -0,0 +1,612 @@
+## markdown
+
+## status: pending
+
+
+engine/infra/sqlite + engine/infra/repo
+implementation
+core_feature
+high
+database
+
+
+# Task 4.0: Task Repository & Factory Integration
+
+## Overview
+
+Implement SQLite-backed task state repository with hierarchical query support and integrate the repository provider factory pattern for multi-driver selection. This is the most complex repository due to parent-child relationships, complex JSONB operations, and array conversions. Also implements the factory pattern to dynamically select PostgreSQL or SQLite repositories based on configuration.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md sections on Task Repository and Factory Pattern
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for test requirements
+- **DEPENDENCY:** Requires Tasks 1.0, 2.0, 3.0 complete
+- **MANDATORY:** Convert PostgreSQL `ANY($1::uuid[])` to SQLite `IN (?, ?, ?)`
+- **MANDATORY:** Handle self-referencing foreign key (parent_state_id)
+- **MANDATORY:** Implement optimistic locking for concurrent updates
+- **MANDATORY:** Factory pattern must not leak driver-specific types
+
+
+
+- Implement `TaskRepo` struct for SQLite
+- Port all methods from `engine/infra/postgres/taskrepo.go`
+- Handle hierarchical queries (parent-child relationships)
+- Convert array operations to SQLite IN clauses
+- Implement optimistic locking with version columns
+- Update `engine/infra/repo/provider.go` with factory pattern
+- Support driver selection: "postgres" | "sqlite"
+- Return interface implementations (not concrete types)
+
+
+## Subtasks
+
+- [ ] 4.1 Create `engine/infra/sqlite/taskrepo.go` structure
+- [ ] 4.2 Implement task state upsert with JSON handling
+- [ ] 4.3 Implement task state retrieval
+- [ ] 4.4 Implement hierarchical list queries
+- [ ] 4.5 Implement array operation conversions
+- [ ] 4.6 Add optimistic locking support
+- [ ] 4.7 Update `engine/infra/repo/provider.go` factory
+- [ ] 4.8 Write unit tests for task operations
+- [ ] 4.9 Write unit tests for hierarchical queries
+- [ ] 4.10 Write unit tests for factory pattern
+- [ ] 4.11 Write integration tests for concurrency
+
+## Implementation Details
+
+### Part A: Task Repository (SQLite)
+
+#### Reference Implementation
+
+**Source:** `engine/infra/postgres/taskrepo.go`
+
+#### 4.1 Repository Structure
+
+```go
+package sqlite
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/compozy/compozy/engine/core"
+ "github.com/compozy/compozy/engine/task"
+)
+
+type TaskRepo struct {
+ db *sql.DB
+}
+
+func NewTaskRepo(db *sql.DB) task.Repository {
+ return &TaskRepo{db: db}
+}
+```
+
+#### 4.2 Upsert State
+
+```go
+func (r *TaskRepo) UpsertState(ctx context.Context, state *task.State) error {
+ // Marshal JSON fields
+ usageJSON, err := marshalJSON(state.Usage)
+ if err != nil {
+ return fmt.Errorf("marshal usage: %w", err)
+ }
+
+ inputJSON, err := marshalJSON(state.Input)
+ if err != nil {
+ return fmt.Errorf("marshal input: %w", err)
+ }
+
+ outputJSON, err := marshalJSON(state.Output)
+ if err != nil {
+ return fmt.Errorf("marshal output: %w", err)
+ }
+
+ errorJSON, err := marshalJSON(state.Error)
+ if err != nil {
+ return fmt.Errorf("marshal error: %w", err)
+ }
+
+ query := `
+ INSERT INTO task_states (
+ task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ agent_id, tool_id, action_id,
+ input, output, error, created_at, updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT (task_exec_id) DO UPDATE SET
+ task_id = excluded.task_id,
+ workflow_exec_id = excluded.workflow_exec_id,
+ workflow_id = excluded.workflow_id,
+ usage = excluded.usage,
+ component = excluded.component,
+ status = excluded.status,
+ execution_type = excluded.execution_type,
+ parent_state_id = excluded.parent_state_id,
+ agent_id = excluded.agent_id,
+ tool_id = excluded.tool_id,
+ action_id = excluded.action_id,
+ input = excluded.input,
+ output = excluded.output,
+ error = excluded.error,
+ updated_at = excluded.updated_at
+ `
+
+ now := time.Now().Format(time.RFC3339)
+
+ _, err = r.db.ExecContext(ctx, query,
+ state.TaskExecID.String(),
+ state.TaskID,
+ state.WorkflowExecID.String(),
+ state.WorkflowID,
+ usageJSON,
+ state.Component,
+ state.Status,
+ state.ExecutionType,
+ nullString(state.ParentStateID),
+ nullString(state.AgentID),
+ nullString(state.ToolID),
+ nullString(state.ActionID),
+ inputJSON,
+ outputJSON,
+ errorJSON,
+ state.CreatedAt.Format(time.RFC3339),
+ now,
+ )
+ if err != nil {
+ return fmt.Errorf("upsert task state: %w", err)
+ }
+
+ return nil
+}
+```
+
+#### 4.3 Get State
+
+```go
+func (r *TaskRepo) GetState(ctx context.Context, taskExecID core.ID) (*task.State, error) {
+ query := `
+ SELECT task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ agent_id, tool_id, action_id,
+ input, output, error, created_at, updated_at
+ FROM task_states
+ WHERE task_exec_id = ?
+ `
+
+ var state task.State
+ var usageJSON, inputJSON, outputJSON, errorJSON sql.NullString
+ var parentID, agentID, toolID, actionID sql.NullString
+ var createdAt, updatedAt string
+
+ err := r.db.QueryRowContext(ctx, query, taskExecID.String()).Scan(
+ &state.TaskExecID,
+ &state.TaskID,
+ &state.WorkflowExecID,
+ &state.WorkflowID,
+ &usageJSON,
+ &state.Component,
+ &state.Status,
+ &state.ExecutionType,
+ &parentID,
+ &agentID,
+ &toolID,
+ &actionID,
+ &inputJSON,
+ &outputJSON,
+ &errorJSON,
+ &createdAt,
+ &updatedAt,
+ )
+ if err == sql.ErrNoRows {
+ return nil, fmt.Errorf("task state not found: %w", core.ErrNotFound)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("get task state: %w", err)
+ }
+
+ // Unmarshal JSON fields
+ unmarshalJSON(usageJSON, &state.Usage)
+ unmarshalJSON(inputJSON, &state.Input)
+ unmarshalJSON(outputJSON, &state.Output)
+ unmarshalJSON(errorJSON, &state.Error)
+
+ // Handle nullable IDs
+ if parentID.Valid {
+ state.ParentStateID = core.ID(parentID.String)
+ }
+ if agentID.Valid {
+ state.AgentID = agentID.String
+ }
+ if toolID.Valid {
+ state.ToolID = toolID.String
+ }
+ if actionID.Valid {
+ state.ActionID = actionID.String
+ }
+
+ // Parse timestamps
+ state.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+ state.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
+
+ return &state, nil
+}
+```
+
+#### 4.4 List with Hierarchy
+
+```go
+func (r *TaskRepo) ListByWorkflow(ctx context.Context, workflowExecID core.ID) ([]*task.State, error) {
+ query := `
+ SELECT task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ agent_id, tool_id, action_id,
+ input, output, error, created_at, updated_at
+ FROM task_states
+ WHERE workflow_exec_id = ?
+ ORDER BY created_at ASC
+ `
+
+ return r.queryTasks(ctx, query, workflowExecID.String())
+}
+
+func (r *TaskRepo) ListChildren(ctx context.Context, parentID core.ID) ([]*task.State, error) {
+ query := `
+ SELECT task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ agent_id, tool_id, action_id,
+ input, output, error, created_at, updated_at
+ FROM task_states
+ WHERE parent_state_id = ?
+ ORDER BY created_at ASC
+ `
+
+ return r.queryTasks(ctx, query, parentID.String())
+}
+
+func (r *TaskRepo) queryTasks(ctx context.Context, query string, args ...any) ([]*task.State, error) {
+ rows, err := r.db.QueryContext(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("query tasks: %w", err)
+ }
+ defer rows.Close()
+
+ var tasks []*task.State
+ for rows.Next() {
+ state, err := r.scanTaskState(rows)
+ if err != nil {
+ return nil, err
+ }
+ tasks = append(tasks, state)
+ }
+
+ return tasks, rows.Err()
+}
+
+func (r *TaskRepo) scanTaskState(scanner interface{ Scan(...any) error }) (*task.State, error) {
+ var state task.State
+ var usageJSON, inputJSON, outputJSON, errorJSON sql.NullString
+ var parentID, agentID, toolID, actionID sql.NullString
+ var createdAt, updatedAt string
+
+ if err := scanner.Scan(
+ &state.TaskExecID,
+ &state.TaskID,
+ &state.WorkflowExecID,
+ &state.WorkflowID,
+ &usageJSON,
+ &state.Component,
+ &state.Status,
+ &state.ExecutionType,
+ &parentID,
+ &agentID,
+ &toolID,
+ &actionID,
+ &inputJSON,
+ &outputJSON,
+ &errorJSON,
+ &createdAt,
+ &updatedAt,
+ ); err != nil {
+ return nil, fmt.Errorf("scan task state: %w", err)
+ }
+
+ // Unmarshal and parse (same as GetState)
+ unmarshalJSON(usageJSON, &state.Usage)
+ unmarshalJSON(inputJSON, &state.Input)
+ unmarshalJSON(outputJSON, &state.Output)
+ unmarshalJSON(errorJSON, &state.Error)
+
+ if parentID.Valid {
+ state.ParentStateID = core.ID(parentID.String)
+ }
+ if agentID.Valid {
+ state.AgentID = agentID.String
+ }
+ if toolID.Valid {
+ state.ToolID = toolID.String
+ }
+ if actionID.Valid {
+ state.ActionID = actionID.String
+ }
+
+ state.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
+ state.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
+
+ return &state, nil
+}
+```
+
+#### 4.5 Array Operations (PostgreSQL `ANY()` β SQLite `IN()`)
+
+```go
+// ListByIDs converts PostgreSQL ANY($1::uuid[]) to SQLite IN (?, ?, ?)
+func (r *TaskRepo) ListByIDs(ctx context.Context, ids []core.ID) ([]*task.State, error) {
+ if len(ids) == 0 {
+ return []*task.State{}, nil
+ }
+
+ // Build placeholders: ?, ?, ?
+ placeholders := make([]string, len(ids))
+ args := make([]any, len(ids))
+ for i, id := range ids {
+ placeholders[i] = "?"
+ args[i] = id.String()
+ }
+
+ query := fmt.Sprintf(`
+ SELECT task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ agent_id, tool_id, action_id,
+ input, output, error, created_at, updated_at
+ FROM task_states
+ WHERE task_exec_id IN (%s)
+ ORDER BY created_at ASC
+ `, strings.Join(placeholders, ", "))
+
+ return r.queryTasks(ctx, query, args...)
+}
+```
+
+#### 4.6 Optimistic Locking (Optional, for concurrent updates)
+
+```go
+// UpdateWithVersion implements optimistic locking
+func (r *TaskRepo) UpdateWithVersion(ctx context.Context, state *task.State, expectedVersion int) error {
+ query := `
+ UPDATE task_states
+ SET status = ?, output = ?, error = ?, version = version + 1, updated_at = ?
+ WHERE task_exec_id = ? AND version = ?
+ `
+
+ outputJSON, _ := marshalJSON(state.Output)
+ errorJSON, _ := marshalJSON(state.Error)
+
+ result, err := r.db.ExecContext(ctx, query,
+ state.Status,
+ outputJSON,
+ errorJSON,
+ time.Now().Format(time.RFC3339),
+ state.TaskExecID.String(),
+ expectedVersion,
+ )
+ if err != nil {
+ return fmt.Errorf("update with version: %w", err)
+ }
+
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return fmt.Errorf("concurrent update detected: %w", core.ErrConflict)
+ }
+
+ return nil
+}
+```
+
+### Part B: Repository Provider Factory
+
+#### 4.7 Factory Pattern (`engine/infra/repo/provider.go`)
+
+**Update existing file:**
+
+```go
+package repo
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/compozy/compozy/engine/auth/uc"
+ "github.com/compozy/compozy/engine/task"
+ "github.com/compozy/compozy/engine/workflow"
+ "github.com/compozy/compozy/engine/infra/postgres"
+ "github.com/compozy/compozy/engine/infra/sqlite"
+ "github.com/compozy/compozy/pkg/config"
+)
+
+type Provider struct {
+ driver string
+ // Don't store concrete types - only use for construction
+}
+
+func NewProvider(ctx context.Context, cfg *config.DatabaseConfig) (*Provider, func(), error) {
+ switch cfg.Driver {
+ case "postgres", "": // default to postgres
+ return newPostgresProvider(ctx, cfg)
+ case "sqlite":
+ return newSQLiteProvider(ctx, cfg)
+ default:
+ return nil, nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
+ }
+}
+
+func newPostgresProvider(ctx context.Context, cfg *config.DatabaseConfig) (*Provider, func(), error) {
+ pgCfg := &postgres.Config{
+ Host: cfg.Host,
+ Port: cfg.Port,
+ User: cfg.User,
+ Password: cfg.Password,
+ DBName: cfg.DBName,
+ SSLMode: cfg.SSLMode,
+ MaxOpenConns: cfg.MaxOpenConns,
+ MaxIdleConns: cfg.MaxIdleConns,
+ }
+
+ store, err := postgres.NewStore(ctx, pgCfg)
+ if err != nil {
+ return nil, nil, fmt.Errorf("create postgres store: %w", err)
+ }
+
+ provider := &Provider{
+ driver: "postgres",
+ authRepo: postgres.NewAuthRepo(store.Pool()),
+ taskRepo: postgres.NewTaskRepo(store.Pool()),
+ workflowRepo: postgres.NewWorkflowRepo(store.Pool()),
+ }
+
+ cleanup := func() {
+ store.Close(ctx)
+ }
+
+ return provider, cleanup, nil
+}
+
+func newSQLiteProvider(ctx context.Context, cfg *config.DatabaseConfig) (*Provider, func(), error) {
+ sqliteCfg := &sqlite.Config{
+ Path: cfg.Path,
+ MaxOpenConns: cfg.MaxOpenConns,
+ MaxIdleConns: cfg.MaxIdleConns,
+ }
+
+ store, err := sqlite.NewStore(ctx, sqliteCfg)
+ if err != nil {
+ return nil, nil, fmt.Errorf("create sqlite store: %w", err)
+ }
+
+ // Apply migrations
+ if err := sqlite.ApplyMigrations(ctx, cfg.Path); err != nil {
+ store.Close(ctx)
+ return nil, nil, fmt.Errorf("apply migrations: %w", err)
+ }
+
+ provider := &Provider{
+ driver: "sqlite",
+ authRepo: sqlite.NewAuthRepo(store.DB()),
+ taskRepo: sqlite.NewTaskRepo(store.DB()),
+ workflowRepo: sqlite.NewWorkflowRepo(store.DB()),
+ }
+
+ cleanup := func() {
+ store.Close(ctx)
+ }
+
+ return provider, cleanup, nil
+}
+
+// Return interface implementations (not concrete types)
+func (p *Provider) NewAuthRepo() uc.Repository {
+ return p.authRepo
+}
+
+func (p *Provider) NewTaskRepo() task.Repository {
+ return p.taskRepo
+}
+
+func (p *Provider) NewWorkflowRepo() workflow.Repository {
+ return p.workflowRepo
+}
+
+func (p *Provider) Driver() string {
+ return p.driver
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `engine/infra/sqlite/taskrepo.go`
+- `engine/infra/sqlite/taskrepo_test.go`
+- `engine/infra/sqlite/helpers.go` (optional, for shared utilities)
+
+**Modified Files:**
+- `engine/infra/repo/provider.go` - Factory pattern implementation
+
+**Reference Files:**
+- `engine/infra/postgres/taskrepo.go` - Source implementation
+- `engine/task/repository.go` - Interface definition
+- `engine/task/state.go` - Task state model
+
+### Dependent Files
+
+- `engine/infra/sqlite/store.go` - Database connection (from Task 1.0)
+- `engine/infra/sqlite/migrations/*.sql` - Schema (from Task 1.0)
+- `engine/infra/sqlite/authrepo.go` - Auth repository (from Task 2.0)
+- `engine/infra/sqlite/workflowrepo.go` - Workflow repository (from Task 3.0)
+
+## Deliverables
+
+- [ ] `engine/infra/sqlite/taskrepo.go` with complete implementation
+- [ ] All task CRUD operations working
+- [ ] Hierarchical queries (parent-child) working
+- [ ] Array operations converted to SQLite IN clauses
+- [ ] Optimistic locking implemented
+- [ ] `engine/infra/repo/provider.go` updated with factory pattern
+- [ ] Factory correctly selects PostgreSQL or SQLite based on config
+- [ ] All unit tests passing for task repository
+- [ ] All unit tests passing for factory pattern
+- [ ] All integration tests passing
+- [ ] Code passes linting
+
+## Tests
+
+### Unit Tests: Task Repository (`engine/infra/sqlite/taskrepo_test.go`)
+
+- [ ] `TestTaskRepo/Should_upsert_task_state`
+- [ ] `TestTaskRepo/Should_get_task_state_by_id`
+- [ ] `TestTaskRepo/Should_list_tasks_by_workflow`
+- [ ] `TestTaskRepo/Should_list_tasks_by_status`
+- [ ] `TestTaskRepo/Should_list_children_of_parent_task`
+- [ ] `TestTaskRepo/Should_list_tasks_by_ids_array`
+- [ ] `TestTaskRepo/Should_handle_jsonb_fields_correctly`
+- [ ] `TestTaskRepo/Should_enforce_foreign_key_to_workflow`
+- [ ] `TestTaskRepo/Should_cascade_delete_children_when_parent_deleted`
+- [ ] `TestTaskRepo/Should_execute_transaction_atomically`
+- [ ] `TestTaskRepo/Should_handle_concurrent_updates`
+- [ ] `TestTaskRepo/Should_implement_optimistic_locking`
+
+### Unit Tests: Factory Pattern (`engine/infra/repo/provider_test.go`)
+
+- [ ] `TestProvider/Should_create_postgres_provider_by_default`
+- [ ] `TestProvider/Should_create_postgres_provider_explicitly`
+- [ ] `TestProvider/Should_create_sqlite_provider`
+- [ ] `TestProvider/Should_return_error_for_invalid_driver`
+- [ ] `TestProvider/Should_return_auth_repository_interface`
+- [ ] `TestProvider/Should_return_task_repository_interface`
+- [ ] `TestProvider/Should_return_workflow_repository_interface`
+- [ ] `TestProvider/Should_not_leak_concrete_types`
+
+### Integration Tests
+
+- [ ] `TestTaskRepo/Should_handle_deep_task_hierarchy`
+- [ ] `TestTaskRepo/Should_handle_complex_json_fields`
+- [ ] `TestTaskRepo/Should_enforce_execution_type_constraints`
+- [ ] `TestTaskRepo/Should_handle_self_referencing_foreign_key`
+
+## Success Criteria
+
+- [ ] All task CRUD operations work correctly
+- [ ] Hierarchical queries return parent-child relationships correctly
+- [ ] Array operations (ListByIDs) work with IN clauses
+- [ ] JSONB fields marshaled/unmarshaled correctly
+- [ ] Foreign keys enforced (workflow_exec_id, parent_state_id)
+- [ ] Cascade deletes work for children when parent deleted
+- [ ] Optimistic locking prevents concurrent update conflicts
+- [ ] Factory pattern selects correct driver based on config
+- [ ] Factory returns interface types (not concrete types)
+- [ ] PostgreSQL repositories still work (zero regression)
+- [ ] All tests pass: `go test ./engine/infra/sqlite/taskrepo_test.go ./engine/infra/repo/provider_test.go`
+- [ ] Code passes linting: `golangci-lint run ./engine/infra/sqlite/taskrepo.go ./engine/infra/repo/provider.go`
diff --git a/tasks/prd-postgres/_task_5.md b/tasks/prd-postgres/_task_5.md
new file mode 100644
index 00000000..6c3326c5
--- /dev/null
+++ b/tasks/prd-postgres/_task_5.md
@@ -0,0 +1,382 @@
+## markdown
+
+## status: pending
+
+
+engine/infra/server
+integration
+core_feature
+medium
+http_server|database
+
+
+# Task 5.0: Server Integration & Validation
+
+## Overview
+
+Integrate SQLite database driver into server initialization and implement critical validation rules. Ensure SQLite deployments cannot use pgvector and provide clear error messages. Add startup warnings for SQLite concurrency limitations.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md section on Integration Points
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for test requirements
+- **DEPENDENCY:** Requires Tasks 1.0, 2.0, 3.0, 4.0 complete
+- **MANDATORY:** SQLite + pgvector must fail at startup with clear error
+- **MANDATORY:** SQLite requires external vector DB (Qdrant/Redis/Filesystem)
+- **MANDATORY:** Log startup information with driver name
+- **MANDATORY:** Warn if SQLite used with high concurrency settings
+
+
+
+- Update `engine/infra/server/dependencies.go` with database setup routing
+- Implement vector DB validation for SQLite
+- Add startup logging with driver information
+- Add concurrency warnings for SQLite
+- Route to PostgreSQL or SQLite store creation based on config
+- Apply migrations automatically on startup
+- Provide clear error messages for misconfigurations
+
+
+## Subtasks
+
+- [ ] 5.1 Update `setupStore()` to route by driver
+- [ ] 5.2 Implement `validateDatabaseConfig()` for vector DB checks
+- [ ] 5.3 Add startup logging with driver information
+- [ ] 5.4 Add concurrency warnings for SQLite
+- [ ] 5.5 Write unit tests for validation logic
+- [ ] 5.6 Write integration tests for server startup
+
+## Implementation Details
+
+### 5.1 Database Setup Routing
+
+**Update:** `engine/infra/server/dependencies.go`
+
+```go
+func (s *Server) setupStore() (*repo.Provider, func(), error) {
+ cfg := config.FromContext(s.ctx)
+
+ // Validate database configuration
+ if err := s.validateDatabaseConfig(cfg); err != nil {
+ return nil, nil, fmt.Errorf("invalid database configuration: %w", err)
+ }
+
+ // Create repository provider (factory handles driver selection)
+ provider, cleanup, err := repo.NewProvider(s.ctx, &cfg.Database)
+ if err != nil {
+ return nil, nil, fmt.Errorf("create repository provider: %w", err)
+ }
+
+ // Log startup information
+ s.logDatabaseStartup(cfg)
+
+ return provider, cleanup, nil
+}
+```
+
+### 5.2 Vector DB Validation
+
+```go
+func (s *Server) validateDatabaseConfig(cfg *config.Config) error {
+ log := logger.FromContext(s.ctx)
+
+ // SQLite-specific validations
+ if cfg.Database.Driver == "sqlite" {
+ // Check if knowledge features are enabled
+ if len(cfg.Knowledge.VectorDBs) == 0 {
+ log.Warn("SQLite mode without vector database - knowledge features will not work",
+ "driver", "sqlite",
+ "recommendation", "Configure Qdrant, Redis, or Filesystem vector DB")
+ // Don't fail - allow running without knowledge features
+ }
+
+ // Ensure no pgvector provider configured
+ for _, vdb := range cfg.Knowledge.VectorDBs {
+ if vdb.Provider == "pgvector" {
+ return fmt.Errorf(
+ "pgvector provider is incompatible with SQLite driver. "+
+ "SQLite requires external vector database. "+
+ "Please configure one of: Qdrant, Redis, or Filesystem. "+
+ "See documentation: docs/database/sqlite.md#vector-database-requirement")
+ }
+ }
+
+ // Warn about concurrency limitations
+ if cfg.Runtime.MaxConcurrentWorkflows > 10 {
+ log.Warn("SQLite has concurrency limitations",
+ "driver", "sqlite",
+ "max_concurrent_workflows", cfg.Runtime.MaxConcurrentWorkflows,
+ "recommended_max", 10,
+ "note", "Consider using PostgreSQL for high-concurrency production workloads")
+ }
+ }
+
+ // PostgreSQL can use any vector DB
+ return nil
+}
+```
+
+### 5.3 Startup Logging
+
+```go
+func (s *Server) logDatabaseStartup(cfg *config.Config) {
+ log := logger.FromContext(s.ctx)
+
+ switch cfg.Database.Driver {
+ case "sqlite", "":
+ if cfg.Database.Driver == "" {
+ cfg.Database.Driver = "postgres" // default
+ }
+ }
+
+ if cfg.Database.Driver == "sqlite" {
+ log.Info("Database initialized",
+ "driver", "sqlite",
+ "path", cfg.Database.Path,
+ "mode", getSQLiteMode(cfg.Database.Path),
+ "vector_db_required", true,
+ "concurrency_limit", "low (5-10 workflows recommended)")
+ } else {
+ log.Info("Database initialized",
+ "driver", "postgres",
+ "host", cfg.Database.Host,
+ "port", cfg.Database.Port,
+ "database", cfg.Database.DBName,
+ "vector_db", "pgvector (optional)",
+ "concurrency_limit", "high (25+ workflows)")
+ }
+}
+
+func getSQLiteMode(path string) string {
+ if path == ":memory:" {
+ return "in-memory"
+ }
+ return "file-based"
+}
+```
+
+### 5.4 Additional Helper Functions
+
+```go
+func buildDatabaseConfig(cfg *config.Config) *config.DatabaseConfig {
+ // Set defaults
+ if cfg.Database.Driver == "" {
+ cfg.Database.Driver = "postgres"
+ }
+
+ if cfg.Database.MaxOpenConns == 0 {
+ cfg.Database.MaxOpenConns = 25
+ }
+
+ if cfg.Database.MaxIdleConns == 0 {
+ cfg.Database.MaxIdleConns = 5
+ }
+
+ return &cfg.Database
+}
+```
+
+### Example Error Messages
+
+**Good Error Message (SQLite + pgvector):**
+```
+Error: pgvector provider is incompatible with SQLite driver.
+
+SQLite requires external vector database for knowledge features.
+Please configure one of the following vector database providers:
+ - Qdrant: See docs/database/sqlite.md#using-qdrant
+ - Redis: See docs/database/sqlite.md#using-redis
+ - Filesystem: See docs/database/sqlite.md#using-filesystem
+
+Example configuration:
+ database:
+ driver: sqlite
+ path: ./data/compozy.db
+
+ knowledge:
+ vector_dbs:
+ - id: main
+ provider: qdrant
+ url: http://localhost:6333
+
+For more information: docs/database/overview.md
+```
+
+### Relevant Files
+
+**Modified Files:**
+- `engine/infra/server/dependencies.go` - Database setup and validation
+
+**Reference Files:**
+- `engine/infra/repo/provider.go` - Factory pattern (from Task 4.0)
+- `pkg/config/config.go` - Configuration (from Task 1.0)
+
+### Dependent Files
+
+- `engine/infra/sqlite/store.go` - SQLite store (from Task 1.0)
+- `engine/infra/postgres/store.go` - PostgreSQL store (existing)
+- `engine/infra/repo/provider.go` - Repository factory (from Task 4.0)
+
+## Deliverables
+
+- [ ] `engine/infra/server/dependencies.go` updated with routing logic
+- [ ] Vector DB validation implemented and working
+- [ ] Startup logging shows correct driver information
+- [ ] Concurrency warnings displayed for SQLite
+- [ ] Clear error messages for misconfigurations
+- [ ] All unit tests passing
+- [ ] All integration tests passing
+- [ ] Code passes linting
+
+## Tests
+
+### Unit Tests (`engine/infra/server/dependencies_test.go`)
+
+- [ ] `TestValidateDatabaseConfig/Should_pass_postgres_with_pgvector`
+- [ ] `TestValidateDatabaseConfig/Should_pass_postgres_without_vector_db`
+- [ ] `TestValidateDatabaseConfig/Should_pass_sqlite_with_qdrant`
+- [ ] `TestValidateDatabaseConfig/Should_pass_sqlite_with_redis`
+- [ ] `TestValidateDatabaseConfig/Should_pass_sqlite_with_filesystem`
+- [ ] `TestValidateDatabaseConfig/Should_fail_sqlite_with_pgvector`
+- [ ] `TestValidateDatabaseConfig/Should_warn_sqlite_without_vector_db`
+- [ ] `TestValidateDatabaseConfig/Should_warn_sqlite_with_high_concurrency`
+
+### Integration Tests
+
+- [ ] `TestServerStartup/Should_start_with_postgres_driver`
+- [ ] `TestServerStartup/Should_start_with_sqlite_driver`
+- [ ] `TestServerStartup/Should_fail_with_invalid_driver`
+- [ ] `TestServerStartup/Should_fail_sqlite_plus_pgvector`
+- [ ] `TestServerStartup/Should_log_driver_information`
+
+### Error Message Tests
+
+```go
+func TestValidateDatabaseConfig(t *testing.T) {
+ t.Run("Should fail sqlite with pgvector", func(t *testing.T) {
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: "./test.db",
+ },
+ Knowledge: config.KnowledgeConfig{
+ VectorDBs: []config.VectorDBConfig{
+ {Provider: "pgvector"},
+ },
+ },
+ }
+
+ err := validateDatabaseConfig(cfg)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "pgvector")
+ assert.Contains(t, err.Error(), "incompatible with SQLite")
+ assert.Contains(t, err.Error(), "Qdrant, Redis, or Filesystem")
+ assert.Contains(t, err.Error(), "docs/database/sqlite.md")
+ })
+
+ t.Run("Should pass sqlite with qdrant", func(t *testing.T) {
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: "./test.db",
+ },
+ Knowledge: config.KnowledgeConfig{
+ VectorDBs: []config.VectorDBConfig{
+ {Provider: "qdrant", URL: "http://localhost:6333"},
+ },
+ },
+ }
+
+ err := validateDatabaseConfig(cfg)
+ assert.NoError(t, err)
+ })
+
+ t.Run("Should warn sqlite with high concurrency", func(t *testing.T) {
+ // Capture logs
+ var logOutput bytes.Buffer
+ log := setupTestLogger(&logOutput)
+ ctx := logger.NewContext(context.Background(), log)
+
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: ":memory:",
+ },
+ Runtime: config.RuntimeConfig{
+ MaxConcurrentWorkflows: 50,
+ },
+ }
+
+ err := validateDatabaseConfig(cfg)
+ assert.NoError(t, err) // Should not fail
+
+ logStr := logOutput.String()
+ assert.Contains(t, logStr, "concurrency limitations")
+ assert.Contains(t, logStr, "recommended_max")
+ assert.Contains(t, logStr, "PostgreSQL")
+ })
+}
+```
+
+### Logging Tests
+
+```go
+func TestDatabaseStartupLogging(t *testing.T) {
+ t.Run("Should log sqlite information", func(t *testing.T) {
+ var logOutput bytes.Buffer
+ log := setupTestLogger(&logOutput)
+ ctx := logger.NewContext(context.Background(), log)
+
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: "./data/compozy.db",
+ },
+ }
+
+ logDatabaseStartup(ctx, cfg)
+
+ logStr := logOutput.String()
+ assert.Contains(t, logStr, "driver=sqlite")
+ assert.Contains(t, logStr, "path=./data/compozy.db")
+ assert.Contains(t, logStr, "mode=file-based")
+ assert.Contains(t, logStr, "vector_db_required=true")
+ })
+
+ t.Run("Should log postgres information", func(t *testing.T) {
+ var logOutput bytes.Buffer
+ log := setupTestLogger(&logOutput)
+ ctx := logger.NewContext(context.Background(), log)
+
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "postgres",
+ Host: "localhost",
+ Port: "5432",
+ DBName: "compozy",
+ },
+ }
+
+ logDatabaseStartup(ctx, cfg)
+
+ logStr := logOutput.String()
+ assert.Contains(t, logStr, "driver=postgres")
+ assert.Contains(t, logStr, "host=localhost")
+ assert.Contains(t, logStr, "vector_db=pgvector")
+ })
+}
+```
+
+## Success Criteria
+
+- [ ] Server starts successfully with PostgreSQL driver
+- [ ] Server starts successfully with SQLite driver
+- [ ] Server fails with clear error when SQLite + pgvector configured
+- [ ] Server warns (but doesn't fail) when SQLite without vector DB
+- [ ] Server warns when SQLite with high concurrency settings
+- [ ] Startup logs show correct driver and configuration information
+- [ ] Error messages are helpful and include documentation links
+- [ ] All validation logic unit tested
+- [ ] All integration tests pass
+- [ ] Code passes linting: `golangci-lint run ./engine/infra/server/...`
+- [ ] Backwards compatibility: Existing PostgreSQL configs work unchanged
diff --git a/tasks/prd-postgres/_task_6.md b/tasks/prd-postgres/_task_6.md
new file mode 100644
index 00000000..10c8b836
--- /dev/null
+++ b/tasks/prd-postgres/_task_6.md
@@ -0,0 +1,476 @@
+## markdown
+
+## status: pending
+
+
+test/integration/database
+testing
+core_feature
+high
+database
+
+
+# Task 6.0: Multi-Driver Integration Tests
+
+## Overview
+
+Create comprehensive parameterized integration tests that run against both PostgreSQL and SQLite drivers, ensuring consistent behavior across database backends. This validates end-to-end workflow execution, task hierarchy, concurrent operations, and database-specific edge cases.
+
+
+- **ALWAYS READ** @.cursor/rules/critical-validation.mdc before start
+- **ALWAYS READ** @.cursor/rules/test-standards.mdc for testing patterns
+- **ALWAYS READ** @tasks/prd-postgres/_tests.md for complete test requirements
+- **DEPENDENCY:** Requires Tasks 1.0-5.0 complete
+- **MANDATORY:** Use `t.Context()` (never `context.Background()`)
+- **MANDATORY:** Test both drivers with same test logic (parameterized tests)
+- **MANDATORY:** Use real databases (no mocks for database operations)
+- **MANDATORY:** Conservative concurrency for SQLite (5-10 workflows max)
+
+
+
+- Parameterized tests for PostgreSQL + SQLite
+- End-to-end workflow execution tests
+- Task hierarchy validation tests
+- Concurrent workflow tests (driver-appropriate limits)
+- SQLite-specific behavior tests (database locking, in-memory mode)
+- Test helpers for multi-driver setup
+- All tests must pass for both drivers
+
+
+## Subtasks
+
+- [ ] 6.1 Create test infrastructure (`test/helpers/database.go`)
+- [ ] 6.2 Implement parameterized workflow execution tests
+- [ ] 6.3 Implement task hierarchy tests
+- [ ] 6.4 Implement concurrent workflow tests
+- [ ] 6.5 Implement SQLite-specific tests
+- [ ] 6.6 Implement transaction tests
+- [ ] 6.7 Implement edge case tests
+
+## Implementation Details
+
+### 6.1 Test Infrastructure
+
+**Create:** `test/helpers/database.go`
+
+```go
+package helpers
+
+import (
+ "context"
+ "testing"
+
+ "github.com/compozy/compozy/engine/infra/repo"
+ "github.com/compozy/compozy/engine/infra/postgres"
+ "github.com/compozy/compozy/engine/infra/sqlite"
+ "github.com/compozy/compozy/pkg/config"
+ "github.com/stretchr/testify/require"
+)
+
+// SetupTestDatabase creates a test database for the specified driver
+func SetupTestDatabase(t *testing.T, driver string) (*repo.Provider, func()) {
+ t.Helper()
+
+ switch driver {
+ case "postgres":
+ return setupPostgresTest(t)
+ case "sqlite":
+ return setupSQLiteTest(t)
+ default:
+ t.Fatalf("unsupported driver: %s", driver)
+ return nil, nil
+ }
+}
+
+func setupSQLiteTest(t *testing.T) (*repo.Provider, func()) {
+ t.Helper()
+
+ // Use in-memory SQLite for fast tests
+ cfg := &config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: ":memory:",
+ }
+
+ provider, cleanup, err := repo.NewProvider(t.Context(), cfg)
+ require.NoError(t, err)
+
+ return provider, cleanup
+}
+
+func setupPostgresTest(t *testing.T) (*repo.Provider, func()) {
+ t.Helper()
+
+ // Use test PostgreSQL (from environment or testcontainer)
+ cfg := &config.DatabaseConfig{
+ Driver: "postgres",
+ Host: getEnvOrDefault("TEST_DB_HOST", "localhost"),
+ Port: getEnvOrDefault("TEST_DB_PORT", "5432"),
+ User: getEnvOrDefault("TEST_DB_USER", "test"),
+ Password: getEnvOrDefault("TEST_DB_PASSWORD", "test"),
+ DBName: getEnvOrDefault("TEST_DB_NAME", "test_compozy"),
+ }
+
+ provider, cleanup, err := repo.NewProvider(t.Context(), cfg)
+ require.NoError(t, err)
+
+ return provider, cleanup
+}
+
+func getEnvOrDefault(key, defaultValue string) string {
+ if val := os.Getenv(key); val != "" {
+ return val
+ }
+ return defaultValue
+}
+```
+
+### 6.2 Workflow Execution Tests
+
+**Create:** `test/integration/database/multi_driver_test.go`
+
+```go
+package database_test
+
+import (
+ "testing"
+
+ "github.com/compozy/compozy/engine/core"
+ "github.com/compozy/compozy/engine/workflow"
+ "github.com/compozy/compozy/test/helpers"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestMultiDriver_WorkflowExecution(t *testing.T) {
+ drivers := []string{"postgres", "sqlite"}
+
+ for _, driver := range drivers {
+ t.Run(driver, func(t *testing.T) {
+ provider, cleanup := helpers.SetupTestDatabase(t, driver)
+ defer cleanup()
+
+ t.Run("Should execute workflow end to end", func(t *testing.T) {
+ testWorkflowExecution(t, provider)
+ })
+
+ t.Run("Should persist task hierarchy", func(t *testing.T) {
+ testTaskHierarchy(t, provider)
+ })
+
+ t.Run("Should handle concurrent workflows", func(t *testing.T) {
+ // Conservative limit for SQLite
+ concurrency := 5
+ if driver == "postgres" {
+ concurrency = 25
+ }
+ testConcurrentWorkflows(t, provider, concurrency)
+ })
+ })
+ }
+}
+
+func testWorkflowExecution(t *testing.T, provider *repo.Provider) {
+ ctx := t.Context()
+ workflowRepo := provider.NewWorkflowRepo()
+
+ // Create workflow state
+ state := &workflow.State{
+ WorkflowExecID: core.NewID(),
+ WorkflowID: "test-workflow",
+ Status: core.StatusRunning,
+ Input: map[string]any{"test": "data"},
+ }
+
+ // Upsert
+ err := workflowRepo.UpsertState(ctx, state)
+ require.NoError(t, err)
+
+ // Retrieve
+ retrieved, err := workflowRepo.GetState(ctx, state.WorkflowExecID)
+ require.NoError(t, err)
+ assert.Equal(t, state.WorkflowID, retrieved.WorkflowID)
+ assert.Equal(t, state.Status, retrieved.Status)
+ assert.Equal(t, state.Input, retrieved.Input)
+
+ // Update status
+ err = workflowRepo.UpdateStatus(ctx, state.WorkflowExecID, core.StatusCompleted)
+ require.NoError(t, err)
+
+ // Verify update
+ retrieved, err = workflowRepo.GetState(ctx, state.WorkflowExecID)
+ require.NoError(t, err)
+ assert.Equal(t, core.StatusCompleted, retrieved.Status)
+}
+```
+
+### 6.3 Task Hierarchy Tests
+
+```go
+func testTaskHierarchy(t *testing.T, provider *repo.Provider) {
+ ctx := t.Context()
+ taskRepo := provider.NewTaskRepo()
+ workflowRepo := provider.NewWorkflowRepo()
+
+ // Create workflow
+ workflowExecID := core.NewID()
+ workflowState := &workflow.State{
+ WorkflowExecID: workflowExecID,
+ WorkflowID: "test-workflow",
+ Status: core.StatusRunning,
+ }
+ err := workflowRepo.UpsertState(ctx, workflowState)
+ require.NoError(t, err)
+
+ // Create parent task
+ parentID := core.NewID()
+ parentTask := &task.State{
+ TaskExecID: parentID,
+ TaskID: "parent-task",
+ WorkflowExecID: workflowExecID,
+ WorkflowID: "test-workflow",
+ Component: "task",
+ Status: core.StatusRunning,
+ ExecutionType: "basic",
+ }
+ err = taskRepo.UpsertState(ctx, parentTask)
+ require.NoError(t, err)
+
+ // Create child tasks
+ child1ID := core.NewID()
+ child1 := &task.State{
+ TaskExecID: child1ID,
+ TaskID: "child-task-1",
+ WorkflowExecID: workflowExecID,
+ WorkflowID: "test-workflow",
+ Component: "task",
+ Status: core.StatusRunning,
+ ExecutionType: "basic",
+ ParentStateID: parentID,
+ }
+ err = taskRepo.UpsertState(ctx, child1)
+ require.NoError(t, err)
+
+ child2ID := core.NewID()
+ child2 := &task.State{
+ TaskExecID: child2ID,
+ TaskID: "child-task-2",
+ WorkflowExecID: workflowExecID,
+ WorkflowID: "test-workflow",
+ Component: "task",
+ Status: core.StatusCompleted,
+ ExecutionType: "basic",
+ ParentStateID: parentID,
+ }
+ err = taskRepo.UpsertState(ctx, child2)
+ require.NoError(t, err)
+
+ // List children
+ children, err := taskRepo.ListChildren(ctx, parentID)
+ require.NoError(t, err)
+ assert.Len(t, children, 2)
+
+ // Verify hierarchy
+ childIDs := []core.ID{children[0].TaskExecID, children[1].TaskExecID}
+ assert.Contains(t, childIDs, child1ID)
+ assert.Contains(t, childIDs, child2ID)
+}
+```
+
+### 6.4 Concurrent Workflow Tests
+
+```go
+func testConcurrentWorkflows(t *testing.T, provider *repo.Provider, concurrency int) {
+ ctx := t.Context()
+ workflowRepo := provider.NewWorkflowRepo()
+
+ // Create multiple workflows concurrently
+ var wg sync.WaitGroup
+ errors := make(chan error, concurrency)
+
+ for i := 0; i < concurrency; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+
+ state := &workflow.State{
+ WorkflowExecID: core.NewID(),
+ WorkflowID: fmt.Sprintf("workflow-%d", idx),
+ Status: core.StatusRunning,
+ }
+
+ if err := workflowRepo.UpsertState(ctx, state); err != nil {
+ errors <- err
+ return
+ }
+
+ // Update status
+ if err := workflowRepo.UpdateStatus(ctx, state.WorkflowExecID, core.StatusCompleted); err != nil {
+ errors <- err
+ }
+ }(i)
+ }
+
+ wg.Wait()
+ close(errors)
+
+ // Check for errors
+ for err := range errors {
+ require.NoError(t, err, "concurrent workflow operation failed")
+ }
+}
+```
+
+### 6.5 SQLite-Specific Tests
+
+**Create:** `test/integration/database/sqlite_specific_test.go`
+
+```go
+package database_test
+
+import (
+ "testing"
+
+ "github.com/compozy/compozy/test/helpers"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestSQLite_Specific(t *testing.T) {
+ provider, cleanup := helpers.SetupTestDatabase(t, "sqlite")
+ defer cleanup()
+
+ t.Run("Should support in memory mode", func(t *testing.T) {
+ // Already using :memory: from test helper
+ // Verify operations work
+ workflowRepo := provider.NewWorkflowRepo()
+
+ state := createTestWorkflowState()
+ err := workflowRepo.UpsertState(t.Context(), state)
+ require.NoError(t, err)
+
+ retrieved, err := workflowRepo.GetState(t.Context(), state.WorkflowExecID)
+ require.NoError(t, err)
+ assert.Equal(t, state.WorkflowID, retrieved.WorkflowID)
+ })
+
+ t.Run("Should enforce foreign keys", func(t *testing.T) {
+ taskRepo := provider.NewTaskRepo()
+
+ // Attempt to create task with non-existent workflow
+ state := &task.State{
+ TaskExecID: core.NewID(),
+ TaskID: "test-task",
+ WorkflowExecID: core.NewID(), // Non-existent workflow
+ WorkflowID: "test-workflow",
+ Component: "task",
+ Status: core.StatusRunning,
+ ExecutionType: "basic",
+ }
+
+ err := taskRepo.UpsertState(t.Context(), state)
+ assert.Error(t, err, "should fail due to foreign key constraint")
+ })
+
+ t.Run("Should handle concurrent reads", func(t *testing.T) {
+ // SQLite should handle many concurrent reads fine
+ workflowRepo := provider.NewWorkflowRepo()
+
+ // Create workflow
+ state := createTestWorkflowState()
+ err := workflowRepo.UpsertState(t.Context(), state)
+ require.NoError(t, err)
+
+ // Concurrent reads
+ var wg sync.WaitGroup
+ for i := 0; i < 100; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ _, err := workflowRepo.GetState(t.Context(), state.WorkflowExecID)
+ require.NoError(t, err)
+ }()
+ }
+
+ wg.Wait()
+ })
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `test/helpers/database.go` - Test database setup helpers
+- `test/integration/database/multi_driver_test.go` - Parameterized tests
+- `test/integration/database/sqlite_specific_test.go` - SQLite edge cases
+- `test/integration/database/workflow_test.go` - Workflow-specific tests
+- `test/integration/database/task_test.go` - Task-specific tests
+- `test/integration/database/transaction_test.go` - Transaction tests
+
+**Reference Files:**
+- `test/helpers/` - Existing test utilities
+- `test/fixtures/` - Test data fixtures
+
+### Dependent Files
+
+- All previous tasks (1.0-5.0) must be complete
+- `engine/infra/repo/provider.go` - Repository factory
+- `engine/infra/sqlite/*` - SQLite repositories
+- `engine/infra/postgres/*` - PostgreSQL repositories
+
+## Deliverables
+
+- [ ] `test/helpers/database.go` with multi-driver setup
+- [ ] Parameterized integration tests for both drivers
+- [ ] End-to-end workflow execution tests
+- [ ] Task hierarchy tests
+- [ ] Concurrent workflow tests (driver-appropriate)
+- [ ] SQLite-specific behavior tests
+- [ ] Transaction tests
+- [ ] All tests pass for both PostgreSQL and SQLite
+- [ ] Test coverage β₯ 80% for new code
+
+## Tests
+
+### Integration Test Categories
+
+**Workflow Execution:**
+- [ ] `TestMultiDriver_WorkflowExecution/Should_execute_workflow_end_to_end`
+- [ ] `TestMultiDriver_WorkflowExecution/Should_persist_task_hierarchy`
+- [ ] `TestMultiDriver_WorkflowExecution/Should_handle_concurrent_workflows`
+
+**Task Operations:**
+- [ ] `TestMultiDriver_TaskOperations/Should_create_and_retrieve_tasks`
+- [ ] `TestMultiDriver_TaskOperations/Should_list_tasks_by_workflow`
+- [ ] `TestMultiDriver_TaskOperations/Should_list_children_of_parent`
+- [ ] `TestMultiDriver_TaskOperations/Should_handle_deep_hierarchy`
+
+**Authentication:**
+- [ ] `TestMultiDriver_Authentication/Should_create_and_retrieve_users`
+- [ ] `TestMultiDriver_Authentication/Should_authenticate_with_api_key`
+- [ ] `TestMultiDriver_Authentication/Should_cascade_delete_api_keys`
+
+**Transactions:**
+- [ ] `TestMultiDriver_Transactions/Should_rollback_on_error`
+- [ ] `TestMultiDriver_Transactions/Should_commit_on_success`
+- [ ] `TestMultiDriver_Transactions/Should_handle_nested_transactions`
+
+**SQLite-Specific:**
+- [ ] `TestSQLite_Specific/Should_support_in_memory_mode`
+- [ ] `TestSQLite_Specific/Should_enforce_foreign_keys`
+- [ ] `TestSQLite_Specific/Should_handle_concurrent_reads`
+- [ ] `TestSQLite_Specific/Should_serialize_concurrent_writes`
+
+## Success Criteria
+
+- [ ] All parameterized tests pass for both PostgreSQL and SQLite
+- [ ] Concurrent workflow tests work (5-10 for SQLite, 25+ for PostgreSQL)
+- [ ] Task hierarchy correctly handled in both databases
+- [ ] Foreign key constraints enforced in both databases
+- [ ] Transactions commit/rollback correctly in both databases
+- [ ] SQLite-specific edge cases handled properly
+- [ ] Test helpers work for both drivers
+- [ ] All tests use `t.Context()` (not `context.Background()`)
+- [ ] All tests follow `t.Run("Should ...")` pattern
+- [ ] No test flakiness or race conditions
+- [ ] Tests run successfully: `go test ./test/integration/database/... -v -race`
+- [ ] Code coverage: `go test -coverprofile=coverage.out ./test/integration/database/...`
diff --git a/tasks/prd-postgres/_task_7.md b/tasks/prd-postgres/_task_7.md
new file mode 100644
index 00000000..61d29ee6
--- /dev/null
+++ b/tasks/prd-postgres/_task_7.md
@@ -0,0 +1,473 @@
+## markdown
+
+## status: pending
+
+
+docs/content/docs
+documentation
+core_feature
+high
+none
+
+
+# Task 7.0: Complete Database Documentation
+
+## Overview
+
+Create comprehensive documentation for the multi-database feature, including decision guides, configuration references, CLI documentation, and troubleshooting guides. This task covers all database-related documentation updates to help users choose the right database and configure it correctly.
+
+
+- **ALWAYS READ** @tasks/prd-postgres/_docs.md for complete documentation plan
+- **ALWAYS READ** @tasks/prd-postgres/_techspec.md for technical details
+- **CAN RUN IN PARALLEL** with implementation tasks (starting Week 3)
+- **MANDATORY:** Include decision matrix for PostgreSQL vs SQLite
+- **MANDATORY:** Emphasize vector DB requirement for SQLite
+- **MANDATORY:** Document concurrency limitations clearly
+- **MANDATORY:** Provide working code examples
+
+
+
+- Create 4 new database documentation pages
+- Update 6+ existing documentation pages
+- Update navigation structure in `source.config.ts`
+- Include decision matrix and comparison tables
+- Provide configuration examples for both drivers
+- Document CLI flags (`--db-driver`, `--db-path`)
+- Create troubleshooting guide for common issues
+- Add diagrams/flowcharts where helpful
+
+
+## Subtasks
+
+- [ ] 7.1 Create `docs/content/docs/database/overview.mdx`
+- [ ] 7.2 Create `docs/content/docs/database/postgresql.mdx`
+- [ ] 7.3 Create `docs/content/docs/database/sqlite.mdx`
+- [ ] 7.4 Create `docs/content/docs/troubleshooting/database.mdx`
+- [ ] 7.5 Update `docs/content/docs/configuration/database.mdx`
+- [ ] 7.6 Update `docs/content/docs/cli/start.mdx`
+- [ ] 7.7 Update `docs/content/docs/cli/migrate.mdx`
+- [ ] 7.8 Update `docs/content/docs/getting-started/installation.mdx`
+- [ ] 7.9 Update `docs/content/docs/getting-started/quickstart.mdx`
+- [ ] 7.10 Update `docs/content/docs/knowledge-bases/vector-databases.mdx`
+- [ ] 7.11 Update `docs/content/docs/deployment/production.mdx`
+- [ ] 7.12 Update navigation in `docs/source.config.ts`
+- [ ] 7.13 Review and test all code examples
+
+## Implementation Details
+
+### 7.1 Database Overview (`docs/content/docs/database/overview.mdx`)
+
+**Content Outline:**
+- Introduction to multi-database support
+- Decision matrix: When to use PostgreSQL vs SQLite
+- Architecture overview (both drivers)
+- Migration considerations
+- Links to specific database docs
+
+**Decision Matrix Table:**
+
+```markdown
+## Choosing Your Database
+
+| Criterion | PostgreSQL β
| SQLite β
|
+|-----------|--------------|----------|
+| **Use Case** | Production, Multi-tenant | Development, Edge, Single-tenant |
+| **Concurrency** | High (100+ workflows) | Low (5-10 workflows) |
+| **Scalability** | Excellent (horizontal/vertical) | Limited (single file) |
+| **Vector Search** | pgvector (built-in) | External DB required |
+| **Deployment** | Separate database server | Embedded (single binary) |
+| **Setup Complexity** | Moderate | Low (just a file path) |
+| **Performance** | Optimized for high load | Optimized for reads |
+| **Backup** | PostgreSQL tools (pg_dump) | File copy |
+| **Recommended For** | β
Production deployments | β
Development, testing, edge |
+
+### When to Use PostgreSQL
+
+Choose PostgreSQL when you need:
+- High concurrency (25+ concurrent workflows)
+- Production-grade reliability and performance
+- Built-in vector search with pgvector
+- Horizontal scaling capabilities
+- Advanced PostgreSQL features
+
+### When to Use SQLite
+
+Choose SQLite when you need:
+- Quick local development setup
+- Single-binary deployment (no external dependencies)
+- Edge/IoT deployments with limited resources
+- Testing and CI/CD pipelines
+- Single-tenant, low-concurrency workloads
+```
+
+### 7.2 PostgreSQL Documentation (`docs/content/docs/database/postgresql.mdx`)
+
+**Content Outline:**
+- PostgreSQL features and benefits
+- Configuration options (connection string, individual params, SSL/TLS)
+- pgvector for knowledge bases
+- Performance tuning
+- Production deployment guide
+- Troubleshooting
+
+**Configuration Example:**
+
+```yaml
+# compozy.yaml (PostgreSQL)
+database:
+ driver: postgres # default, can be omitted
+ host: localhost
+ port: 5432
+ user: compozy
+ password: ${DB_PASSWORD} # from environment
+ dbname: compozy
+ sslmode: require
+ max_open_conns: 25
+ max_idle_conns: 5
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: pgvector # Uses PostgreSQL
+ dimension: 1536
+```
+
+### 7.3 SQLite Documentation (`docs/content/docs/database/sqlite.mdx`)
+
+**Content Outline:**
+- SQLite features and limitations
+- Ideal use cases (development, testing, edge)
+- Configuration options (file-based, in-memory, PRAGMA settings)
+- **CRITICAL: Vector Database Requirement** (highlighted section)
+- Performance characteristics
+- Concurrency limitations (5-10 workflows recommended)
+- Backup and export
+- Troubleshooting
+
+**CRITICAL Section:**
+
+```markdown
+## β οΈ Vector Database Requirement
+
+SQLite does **not have native vector database support**. If you plan to use knowledge bases or RAG features, you **must configure an external vector database**.
+
+### Supported Vector Databases
+
+When using SQLite, configure one of the following:
+
+1. **Qdrant** (Recommended for production)
+2. **Redis** with RediSearch
+3. **Filesystem** (Development only)
+
+### Example Configuration
+
+```yaml
+database:
+ driver: sqlite
+ path: ./data/compozy.db
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: qdrant # External vector DB required
+ url: http://localhost:6333
+ dimension: 1536
+```
+
+### Why Not pgvector?
+
+The `pgvector` provider is **incompatible** with SQLite. If you attempt to configure SQLite with pgvector, Compozy will fail at startup with a clear error message.
+```
+
+**Concurrency Limitations:**
+
+```markdown
+## Concurrency Limitations
+
+SQLite uses **database-level locking** (not row-level), which limits write concurrency:
+
+- β
**Recommended:** 5-10 concurrent workflows
+- β οΈ **Not recommended:** 25+ concurrent workflows
+- π΄ **High-concurrency production:** Use PostgreSQL instead
+
+SQLite is designed for:
+- Single-tenant applications
+- Development and testing
+- Edge deployments with moderate load
+```
+
+### 7.4 Troubleshooting Guide (`docs/content/docs/troubleshooting/database.mdx`)
+
+**Content Outline:**
+- Common PostgreSQL issues (connection failures, migration errors, performance)
+- Common SQLite issues (database locked, concurrency, file permissions, vector DB not configured)
+- Diagnostic commands
+- Error messages reference
+
+**Example Sections:**
+
+```markdown
+## SQLite: Database Locked Error
+
+**Error:** `database is locked`
+
+**Cause:** Multiple processes/threads attempting concurrent writes
+
+**Solution:**
+1. Reduce concurrent workflow limit in configuration
+2. Implement retry logic with exponential backoff
+3. Consider PostgreSQL for high-concurrency workloads
+
+```yaml
+runtime:
+ max_concurrent_workflows: 5 # Conservative for SQLite
+```
+
+---
+
+## SQLite: Vector DB Not Configured
+
+**Error:** `pgvector provider is incompatible with SQLite driver`
+
+**Cause:** Attempted to use pgvector with SQLite
+
+**Solution:** Configure an external vector database:
+
+```yaml
+database:
+ driver: sqlite
+ path: ./data/compozy.db
+
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: qdrant # Use external vector DB
+ url: http://localhost:6333
+```
+
+See: [SQLite Vector Database Requirement](/docs/database/sqlite#vector-database-requirement)
+```
+
+### 7.5 Update Configuration Reference (`docs/content/docs/configuration/database.mdx`)
+
+**Add Section:**
+
+```markdown
+## Database Driver Selection
+
+### `database.driver`
+
+Select the database backend.
+
+- **Type:** `string`
+- **Options:** `postgres` | `sqlite`
+- **Default:** `postgres`
+- **Environment Variable:** `DB_DRIVER`
+
+### PostgreSQL Configuration
+
+When `driver: postgres` (or omitted), configure PostgreSQL-specific fields:
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `host` | string | Yes* | PostgreSQL server host |
+| `port` | string | No | PostgreSQL server port (default: 5432) |
+| `user` | string | Yes | Database user |
+| `password` | string | Yes | Database password |
+| `dbname` | string | Yes | Database name |
+| `sslmode` | string | No | SSL mode (disable, require, verify-ca, verify-full) |
+
+*Or provide `conn_string` instead
+
+### SQLite Configuration
+
+When `driver: sqlite`, configure SQLite-specific fields:
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `path` | string | Yes | Database file path or `:memory:` |
+
+**Example:**
+
+```yaml
+database:
+ driver: sqlite
+ path: ./data/compozy.db # File-based
+ # OR
+ path: ":memory:" # In-memory (ephemeral)
+```
+```
+
+### 7.6 Update CLI Start Documentation (`docs/content/docs/cli/start.mdx`)
+
+**Add Section:**
+
+```markdown
+## Database Options
+
+### `--db-driver`
+
+Select database driver.
+
+- **Values:** `postgres` | `sqlite`
+- **Default:** `postgres`
+
+**Examples:**
+
+```bash
+# PostgreSQL (default)
+compozy start
+
+# SQLite (file-based)
+compozy start --db-driver=sqlite --db-path=./compozy.db
+
+# SQLite (in-memory)
+compozy start --db-driver=sqlite --db-path=:memory:
+```
+
+### `--db-path`
+
+SQLite database file path (required when `--db-driver=sqlite`).
+
+**Examples:**
+
+```bash
+# File-based database
+compozy start --db-driver=sqlite --db-path=./data/compozy.db
+
+# In-memory database (data lost on restart)
+compozy start --db-driver=sqlite --db-path=:memory:
+```
+```
+
+### 7.7 Update CLI Migrate Documentation (`docs/content/docs/cli/migrate.mdx`)
+
+**Add Section:**
+
+```markdown
+## Multi-Database Support
+
+Migrations work with both PostgreSQL and SQLite. The driver is automatically detected from your configuration.
+
+```bash
+# Apply migrations (auto-detects driver from compozy.yaml)
+compozy migrate up
+
+# Check migration status
+compozy migrate status
+
+# Rollback migrations
+compozy migrate down
+```
+
+**Note:** PostgreSQL and SQLite use separate migration files optimized for each database's SQL dialect.
+```
+
+### 7.8-7.11 Update Cross-Page Documentation
+
+**Updates:**
+- `getting-started/installation.mdx`: Add "Quick Start with SQLite" section
+- `getting-started/quickstart.mdx`: Add "5-Minute Setup (SQLite)" path
+- `knowledge-bases/vector-databases.mdx`: Add note about SQLite requirement
+- `deployment/production.mdx`: Emphasize PostgreSQL recommendation
+
+### 7.12 Update Navigation (`docs/source.config.ts`)
+
+**Add Database Section:**
+
+```typescript
+{
+ title: "Database",
+ pages: [
+ "database/overview", // NEW
+ "database/postgresql", // NEW
+ "database/sqlite", // NEW
+ ]
+},
+{
+ title: "Troubleshooting",
+ pages: [
+ "troubleshooting/overview",
+ "troubleshooting/database", // NEW
+ "troubleshooting/workflows",
+ ]
+}
+```
+
+### Relevant Files
+
+**New Files:**
+- `docs/content/docs/database/overview.mdx`
+- `docs/content/docs/database/postgresql.mdx`
+- `docs/content/docs/database/sqlite.mdx`
+- `docs/content/docs/troubleshooting/database.mdx`
+
+**Modified Files:**
+- `docs/content/docs/configuration/database.mdx`
+- `docs/content/docs/cli/start.mdx`
+- `docs/content/docs/cli/migrate.mdx`
+- `docs/content/docs/getting-started/installation.mdx`
+- `docs/content/docs/getting-started/quickstart.mdx`
+- `docs/content/docs/knowledge-bases/vector-databases.mdx`
+- `docs/content/docs/deployment/production.mdx`
+- `docs/source.config.ts`
+
+### Dependent Files
+
+- None (documentation can be written in parallel with implementation)
+
+## Deliverables
+
+- [ ] 4 new database documentation pages created
+- [ ] 7+ existing pages updated with database references
+- [ ] Navigation structure updated in `source.config.ts`
+- [ ] Decision matrix included in overview
+- [ ] Vector DB requirement clearly documented for SQLite
+- [ ] Concurrency limitations documented
+- [ ] All configuration examples tested and working
+- [ ] CLI flags documented
+- [ ] Troubleshooting guide complete
+- [ ] All internal links working
+- [ ] Documentation builds without errors: `npm run dev`
+
+## Tests
+
+### Documentation Quality Checks
+
+- [ ] All code examples are valid and tested
+- [ ] All internal links resolve correctly (no 404s)
+- [ ] Search functionality finds new database pages
+- [ ] Mobile view renders correctly
+- [ ] Dark mode styling consistent
+- [ ] Syntax highlighting works for code blocks
+
+### Manual Testing Checklist
+
+- [ ] Follow PostgreSQL setup guide β successful workflow execution
+- [ ] Follow SQLite setup guide β successful workflow execution
+- [ ] Try invalid config (SQLite + pgvector) β clear error message matches docs
+- [ ] Copy/paste config examples β they work as-is
+- [ ] Click all internal database links β no broken links
+
+### Automated Checks
+
+- [ ] Link checker passes: `npm run check-links`
+- [ ] Build completes without warnings: `npm run build`
+- [ ] No broken schema references
+- [ ] No typos in code examples
+
+## Success Criteria
+
+- [ ] All new documentation pages created and complete
+- [ ] All updated pages reflect multi-database support
+- [ ] Decision matrix helps users choose database
+- [ ] Vector DB requirement for SQLite clearly documented and emphasized
+- [ ] Configuration examples work when copy-pasted
+- [ ] CLI documentation includes new flags
+- [ ] Troubleshooting guide covers common issues
+- [ ] Navigation structure logical and easy to follow
+- [ ] All internal links work
+- [ ] Documentation builds successfully: `npm run build`
+- [ ] Search indexes new pages: search for "sqlite" returns relevant results
+- [ ] Mobile and dark mode rendering correct
+- [ ] Code examples follow project standards
diff --git a/tasks/prd-postgres/_task_8.md b/tasks/prd-postgres/_task_8.md
new file mode 100644
index 00000000..68432e61
--- /dev/null
+++ b/tasks/prd-postgres/_task_8.md
@@ -0,0 +1,444 @@
+## markdown
+
+## status: pending
+
+
+examples/database
+documentation
+core_feature
+low
+none
+
+
+# Task 8.0: SQLite Quickstart Example
+
+## Overview
+
+Create a simple, working example demonstrating SQLite backend usage with a basic workflow. This example shows the easiest path to get started with Compozy using SQLite, requiring minimal setup and no external database dependencies.
+
+
+- **ALWAYS READ** @tasks/prd-postgres/_examples.md for example structure
+- **ALWAYS READ** @examples/prompt-only/ for reference pattern
+- **DEPENDENCY:** Can start after Task 1.0 (Foundation) is complete
+- **MANDATORY:** Use filesystem vector DB (no external dependencies)
+- **MANDATORY:** Simple workflow (like prompt-only example)
+- **MANDATORY:** Clear README with step-by-step instructions
+- **MANDATORY:** Working configuration that can be copy-pasted
+
+
+
+- Create `examples/database/sqlite-quickstart/` directory
+- SQLite configuration with file-based database
+- Filesystem vector DB (no external services)
+- Simple text analysis workflow
+- README with setup instructions
+- `.env.example` for API keys
+- Runnable with `compozy start`
+
+
+## Subtasks
+
+- [ ] 8.1 Create example directory structure
+- [ ] 8.2 Create `compozy.yaml` with SQLite configuration
+- [ ] 8.3 Create simple workflow (`workflow.yaml`)
+- [ ] 8.4 Write comprehensive README
+- [ ] 8.5 Create `.env.example` file
+- [ ] 8.6 Test example end-to-end
+- [ ] 8.7 Add example to main examples index
+
+## Implementation Details
+
+### 8.1 Directory Structure
+
+```
+examples/database/sqlite-quickstart/
+βββ compozy.yaml # SQLite configuration
+βββ workflow.yaml # Simple text analysis workflow
+βββ README.md # Setup and run instructions
+βββ .env.example # API key placeholder
+βββ data/ # Created automatically for SQLite database
+```
+
+### 8.2 SQLite Configuration (`compozy.yaml`)
+
+**Reference:** `examples/prompt-only/compozy.yaml`
+
+```yaml
+name: sqlite-quickstart
+version: 0.1.0
+description: Minimal SQLite database backend example
+
+workflows:
+ - source: ./workflow.yaml
+
+models:
+ - provider: groq
+ model: llama-3.3-70b-versatile
+ api_key: "{{ .env.GROQ_API_KEY }}"
+ default: true
+
+# SQLite database configuration
+database:
+ driver: sqlite
+ path: ./data/compozy.db # File-based database
+
+# Filesystem vector DB (no external dependencies)
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: filesystem
+ path: ./data/vectors
+ dimension: 1536
+
+runtime:
+ type: bun
+ permissions:
+ - --allow-read
+ - --allow-net
+```
+
+### 8.3 Simple Workflow (`workflow.yaml`)
+
+**Reference:** `examples/prompt-only/workflow.yaml`
+
+```yaml
+id: text-analysis
+version: 0.1.0
+description: Simple text analysis workflow using SQLite backend
+
+config:
+ input:
+ type: object
+ properties:
+ text:
+ type: string
+ description: The text content to analyze
+
+tasks:
+ - id: analyze
+ type: basic
+ prompt: |-
+ You are a concise text analysis assistant.
+
+ Analyze the following text and provide:
+ 1. A brief summary (1-2 sentences)
+ 2. Key themes or topics
+ 3. Overall sentiment (positive/negative/neutral)
+
+ Text to analyze:
+ ---
+ {{ .workflow.input.text }}
+ ---
+
+ Provide your analysis in a clear, structured format.
+```
+
+### 8.4 README (`README.md`)
+
+```markdown
+# SQLite Quickstart Example
+
+Minimal example demonstrating Compozy with SQLite database backend.
+
+## Features
+
+- β
**No external database required** - SQLite embedded
+- β
**Single file database** - All data in `./data/compozy.db`
+- β
**Filesystem vector DB** - No Qdrant/Redis needed
+- β
**Simple setup** - Just configure API key and run
+
+## Prerequisites
+
+- Compozy CLI installed
+- LLM API key (Groq, OpenAI, or other provider)
+
+## Quick Start
+
+### 1. Configure API Key
+
+```bash
+cp .env.example .env
+# Edit .env and add your API key
+```
+
+### 2. Start Compozy
+
+```bash
+compozy start
+```
+
+The SQLite database will be created automatically at `./data/compozy.db`.
+
+**You should see:**
+```
+Database initialized: driver=sqlite path=./data/compozy.db mode=file-based
+Server listening on :5001
+```
+
+### 3. Run the Workflow
+
+In another terminal:
+
+```bash
+compozy workflow run text-analysis --input='{"text": "Compozy makes AI workflows easy!"}'
+```
+
+**Expected output:**
+```json
+{
+ "data": {
+ "exec_id": "...",
+ "status": "completed",
+ "output": {
+ "analysis": "Summary: ...",
+ "themes": ["..."],
+ "sentiment": "positive"
+ }
+ }
+}
+```
+
+### 4. Verify Database
+
+```bash
+# Check database file created
+ls -lh ./data/compozy.db
+
+# View workflow history
+compozy workflow list
+```
+
+## Configuration Details
+
+### SQLite Database
+
+```yaml
+database:
+ driver: sqlite
+ path: ./data/compozy.db # File-based storage
+```
+
+- **File-based:** Data persists across restarts
+- **Location:** `./data/compozy.db` (created automatically)
+- **No external dependencies:** Single binary + database file
+
+### Filesystem Vector DB
+
+```yaml
+knowledge:
+ vector_dbs:
+ - id: main
+ provider: filesystem
+ path: ./data/vectors
+```
+
+- **No external services:** Vectors stored as files
+- **Development-friendly:** No Qdrant/Redis setup needed
+- **Note:** For production, use Qdrant or Redis
+
+## When to Use This Setup
+
+β
**Good for:**
+- Local development
+- Quick evaluation/testing
+- CI/CD pipelines
+- Edge deployments
+- Single-tenant applications
+
+β οΈ **Not recommended for:**
+- High-concurrency production (use PostgreSQL)
+- Multi-tenant applications (use PostgreSQL)
+- Workloads with 25+ concurrent workflows
+
+## Comparison: SQLite vs PostgreSQL
+
+| Feature | This Example (SQLite) | PostgreSQL |
+|---------|----------------------|------------|
+| Setup Time | < 1 minute | ~5 minutes |
+| External Dependencies | None | PostgreSQL server |
+| Concurrency | Low (5-10 workflows) | High (100+ workflows) |
+| Vector Search | Filesystem (basic) | pgvector (advanced) |
+| Production Use | Edge/Single-tenant | Multi-tenant/Scale |
+
+## Next Steps
+
+- **Production deployment:** See [PostgreSQL setup guide](../../docs/database/postgresql.md)
+- **Vector search:** Configure [Qdrant](../../docs/database/sqlite.md#using-qdrant)
+- **More examples:** Browse [examples directory](../)
+
+## Troubleshooting
+
+### Database file not created
+
+**Check:** Is the `./data/` directory writable?
+
+```bash
+mkdir -p ./data
+compozy start
+```
+
+### Vector DB errors
+
+**Note:** Filesystem vector DB is for development only. For production, configure Qdrant or Redis.
+
+### Permission errors
+
+**Check:** Bun runtime permissions in `compozy.yaml`:
+
+```yaml
+runtime:
+ type: bun
+ permissions:
+ - --allow-read
+ - --allow-net
+```
+
+## Learn More
+
+- [Database Overview](../../docs/database/overview.md)
+- [SQLite Configuration](../../docs/database/sqlite.md)
+- [Vector Databases](../../docs/knowledge-bases/vector-databases.md)
+```
+
+### 8.5 Environment Variables (`.env.example`)
+
+```bash
+# LLM Provider API Key
+GROQ_API_KEY=your_api_key_here
+
+# Or use OpenAI
+# OPENAI_API_KEY=your_api_key_here
+```
+
+### 8.6 Testing the Example
+
+**Manual Test Steps:**
+
+```bash
+# 1. Navigate to example
+cd examples/database/sqlite-quickstart
+
+# 2. Setup environment
+cp .env.example .env
+# Edit .env with your API key
+
+# 3. Start Compozy
+compozy start
+
+# Expected: Database initialized message
+
+# 4. In another terminal, run workflow
+compozy workflow run text-analysis --input='{"text": "Test content"}'
+
+# Expected: Workflow completes successfully
+
+# 5. Verify database created
+ls -lh ./data/compozy.db
+
+# Expected: File exists (size > 0)
+
+# 6. List workflows
+compozy workflow list
+
+# Expected: See executed workflow
+
+# 7. Stop server
+# Ctrl+C in terminal running compozy start
+
+# 8. Restart and verify persistence
+compozy start
+compozy workflow list
+
+# Expected: Previous workflow still listed
+```
+
+### 8.7 Update Examples Index
+
+**Add to:** `examples/README.md` (or main examples index)
+
+```markdown
+### Database Examples
+
+#### SQLite Quickstart
+
+**Location:** `database/sqlite-quickstart/`
+
+Minimal example demonstrating SQLite backend with filesystem vector DB. Perfect for local development and testing.
+
+**Features:**
+- No external database dependencies
+- Single-file database
+- Quick setup (< 1 minute)
+
+**Use Case:** Development, testing, edge deployments
+
+[View Example β](./database/sqlite-quickstart/)
+```
+
+### Relevant Files
+
+**New Files:**
+- `examples/database/sqlite-quickstart/compozy.yaml`
+- `examples/database/sqlite-quickstart/workflow.yaml`
+- `examples/database/sqlite-quickstart/README.md`
+- `examples/database/sqlite-quickstart/.env.example`
+
+**Modified Files:**
+- `examples/README.md` (add reference to new example)
+
+**Reference Files:**
+- `examples/prompt-only/` - Similar structure and patterns
+
+### Dependent Files
+
+- `engine/infra/sqlite/` - SQLite implementation (from Task 1.0)
+- `engine/infra/repo/provider.go` - Factory pattern (from Task 4.0)
+
+## Deliverables
+
+- [ ] `examples/database/sqlite-quickstart/` directory created
+- [ ] `compozy.yaml` with SQLite configuration
+- [ ] `workflow.yaml` with simple text analysis task
+- [ ] `README.md` with comprehensive setup instructions
+- [ ] `.env.example` with API key placeholder
+- [ ] Example runs successfully end-to-end
+- [ ] Database file created at `./data/compozy.db`
+- [ ] Workflow executes and completes
+- [ ] Added to examples index
+
+## Tests
+
+### Manual Testing Checklist
+
+- [ ] Navigate to example directory
+- [ ] Create `.env` from `.env.example`
+- [ ] Add valid API key to `.env`
+- [ ] Run `compozy start` - server starts successfully
+- [ ] Database file created at `./data/compozy.db`
+- [ ] Run workflow - completes successfully
+- [ ] Run `compozy workflow list` - shows executed workflow
+- [ ] Stop server (Ctrl+C)
+- [ ] Restart server - data persists
+- [ ] README instructions accurate and complete
+- [ ] All code examples work when copy-pasted
+- [ ] No errors in logs
+
+### Edge Cases
+
+- [ ] Run without `.env` file - clear error message
+- [ ] Run with invalid API key - clear error message
+- [ ] Run with missing `./data/` directory - creates automatically
+- [ ] Run twice - database not recreated, data preserved
+
+## Success Criteria
+
+- [ ] Example runs successfully from scratch
+- [ ] Database file created automatically
+- [ ] Workflow executes and persists to database
+- [ ] Data survives server restart
+- [ ] README instructions complete and accurate
+- [ ] All configuration examples work as-is
+- [ ] No external dependencies required (except LLM API)
+- [ ] Example demonstrates key SQLite features (file-based DB, persistence)
+- [ ] Clear documentation on when to use SQLite vs PostgreSQL
+- [ ] Example added to main examples index
diff --git a/tasks/prd-postgres/_tasks.md b/tasks/prd-postgres/_tasks.md
new file mode 100644
index 00000000..e1bc4339
--- /dev/null
+++ b/tasks/prd-postgres/_tasks.md
@@ -0,0 +1,133 @@
+# SQLite Database Backend Support - Implementation Tasks
+
+## Relevant Files
+
+### Core Implementation Files
+
+- `pkg/config/config.go` - Database driver configuration
+- `engine/infra/sqlite/store.go` - SQLite connection management
+- `engine/infra/sqlite/migrations.go` - Migration system
+- `engine/infra/sqlite/migrations/*.sql` - SQLite schema definitions
+- `engine/infra/sqlite/authrepo.go` - Authentication repository
+- `engine/infra/sqlite/workflowrepo.go` - Workflow state repository
+- `engine/infra/sqlite/taskrepo.go` - Task state repository
+- `engine/infra/repo/provider.go` - Repository factory pattern
+
+### Integration Points
+
+- `engine/infra/server/dependencies.go` - Server initialization and validation
+- `test/integration/database/multi_driver_test.go` - Cross-driver integration tests
+- `test/helpers/database.go` - Test infrastructure
+
+### Documentation Files
+
+- `docs/content/docs/database/overview.mdx` - Database decision guide
+- `docs/content/docs/database/postgresql.mdx` - PostgreSQL documentation
+- `docs/content/docs/database/sqlite.mdx` - SQLite documentation
+- `docs/content/docs/troubleshooting/database.mdx` - Troubleshooting guide
+- `docs/content/docs/configuration/database.mdx` - Configuration reference
+- `docs/content/docs/cli/start.mdx` - CLI documentation
+- `docs/source.config.ts` - Navigation structure
+
+### Examples
+
+- `examples/database/sqlite-quickstart/` - SQLite quickstart example
+
+## Tasks
+
+- [ ] 1.0 SQLite Foundation Infrastructure (L)
+- [ ] 2.0 Authentication Repository (SQLite) (M)
+- [ ] 3.0 Workflow Repository (SQLite) (M)
+- [ ] 4.0 Task Repository & Factory Integration (L)
+- [ ] 5.0 Server Integration & Validation (M)
+- [ ] 6.0 Multi-Driver Integration Tests (L)
+- [ ] 7.0 Complete Database Documentation (L)
+- [ ] 8.0 SQLite Quickstart Example (S)
+
+Notes on sizing:
+
+- S = Small (β€ half-day)
+- M = Medium (1β2 days)
+- L = Large (3+ days)
+
+## Task Design Rules
+
+- Each parent task is a closed deliverable: independently shippable and reviewable
+- Do not split one deliverable across multiple parent tasks; avoid cross-task coupling
+- Each parent task must include unit test subtasks derived from `_tests.md` for this feature
+- Each generated `/_task_.md` must contain explicit Deliverables and Tests sections
+
+## Execution Plan
+
+### Critical Path (Sequential)
+```
+1.0 β 2.0 β 3.0 β 4.0 β 5.0
+```
+**Duration:** ~12-18 days
+
+### Parallel Tracks
+
+**Track A (Documentation):** Can start Week 3
+```
+7.0
+```
+
+**Track B (Testing):** Can start after Task 5.0
+```
+6.0
+```
+
+**Track C (Example):** Can start Week 7
+```
+8.0
+```
+
+### Timeline
+- **Weeks 1-2:** Foundation (Task 1.0)
+- **Weeks 2-5:** Repositories (Tasks 2.0, 3.0, 4.0)
+- **Weeks 5-6:** Integration (Task 5.0)
+- **Week 6:** Testing (Task 6.0)
+- **Weeks 3-7:** Documentation (Task 7.0, parallel)
+- **Week 7:** Example (Task 8.0)
+
+**Total Duration:** ~4-6 weeks
+
+## Notes
+
+- All runtime code MUST use `logger.FromContext(ctx)` and `config.FromContext(ctx)`
+- Run `make fmt && make lint && make test` before marking any task as completed
+- Use `t.Context()` in tests (never `context.Background()`)
+- Follow repository pattern with interfaces (no leaking of driver-specific types)
+- PostgreSQL remains default driver - zero breaking changes
+- Vector DB validation: SQLite + pgvector must fail at startup
+
+## Batch Plan (Grouped Commits)
+
+- [ ] Batch 1 β Foundation: 1.0
+- [ ] Batch 2 β Repositories: 2.0, 3.0, 4.0
+- [ ] Batch 3 β Integration: 5.0
+- [ ] Batch 4 β Testing: 6.0
+- [ ] Batch 5 β Documentation: 7.0
+- [ ] Batch 6 β Example: 8.0
+
+## Key Technical Decisions
+
+**From Technical Specification:**
+- **Driver Choice:** `modernc.org/sqlite` (pure Go, no CGO)
+- **Architecture:** Hybrid dual-implementation (separate `postgres/` and `sqlite/` packages)
+- **Vector Storage:** Mandatory external vector DB for SQLite (Qdrant/Redis/Filesystem)
+- **Concurrency:** Document limitations (5-10 workflows recommended for SQLite)
+- **SQL Syntax:** Separate migration files for PostgreSQL and SQLite
+- **Locking:** Optimistic locking with version columns (SQLite has DB-level locking only)
+
+## Success Criteria
+
+- [ ] All tests pass: `make test`
+- [ ] All linters pass: `make lint`
+- [ ] PostgreSQL tests unchanged (zero regressions)
+- [ ] SQLite tests achieve 80%+ coverage
+- [ ] Integration tests pass for both drivers
+- [ ] Documentation complete and reviewed
+- [ ] Example runs successfully
+- [ ] Vector DB validation enforced
+- [ ] No breaking changes to existing configurations
diff --git a/tasks/prd-postgres/_techspec.md b/tasks/prd-postgres/_techspec.md
new file mode 100644
index 00000000..5ef8f658
--- /dev/null
+++ b/tasks/prd-postgres/_techspec.md
@@ -0,0 +1,1194 @@
+# Technical Specification: SQLite Database Backend Support
+
+## Executive Summary
+
+This specification details the implementation strategy for adding SQLite as an alternative database backend to Compozy. The solution uses a **Hybrid Dual-Implementation approach**: maintaining separate PostgreSQL and SQLite implementations behind a unified repository provider pattern, while requiring external vector databases (Qdrant/Redis/Filesystem) when SQLite is selected. PostgreSQL remains the default and recommended option for production workloads.
+
+**Key Architectural Decisions:**
+- **Dual Implementation:** Separate `engine/infra/postgres` and `engine/infra/sqlite` packages
+- **Factory Pattern:** `engine/infra/repo.Provider` selects implementation based on configuration
+- **Vector DB Separation:** SQLite deployments mandate external vector database
+- **Zero Breaking Changes:** Existing PostgreSQL code unchanged, fully backwards compatible
+
+## System Architecture
+
+### Domain Placement
+
+**New Components:**
+- `engine/infra/sqlite/` - SQLite driver implementation (parallel to `postgres/`)
+ - `store.go` - Connection pool management
+ - `authrepo.go` - User/API key repository
+ - `taskrepo.go` - Task state repository
+ - `workflowrepo.go` - Workflow state repository
+ - `migrations/` - SQLite-specific migration files
+ - `helpers.go` - SQLite-specific query utilities
+
+**Modified Components:**
+- `engine/infra/repo/provider.go` - Factory selection logic
+- `pkg/config/config.go` - Database driver configuration
+- `engine/infra/server/dependencies.go` - Database setup routing
+
+**No Changes Required:**
+- `engine/workflow`, `engine/task`, `engine/auth` - Domain interfaces unchanged
+- `engine/knowledge/vectordb` - Already supports multiple providers
+
+### Component Overview
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Configuration Layer (pkg/config) β
+β - DatabaseConfig.Driver: "postgres" | "sqlite" β
+β - Driver-specific fields (PostgreSQL: DSN, SQLite: Path)β
+βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Application Layer (Domain Repositories) β
+β - workflow.Repository (interface) β
+β - task.Repository (interface) β
+β - auth.Repository (interface) β
+βββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
+ β
+ βΌ
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Infrastructure: Repository Provider (Factory) β
+β engine/infra/repo/provider.go β
+β β
+β func NewProvider(cfg *DatabaseConfig) *Provider β
+β switch cfg.Driver { β
+β case "postgres": β postgres repositories β
+β case "sqlite": β sqlite repositories β
+β } β
+βββββββββ¬ββββββββββββββββββββββββββββββββββ¬ββββββββββββββββ
+ β β
+ βΌ βΌ
+ββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
+β PostgreSQL Driver β β SQLite Driver β
+β engine/infra/postgresβ β engine/infra/sqlite β
+β - Uses pgx/pgxpool β β - Uses modernc.org/sqliteβ
+β - pgvector support β β - Pure Go, no CGO β
+β - Row-level locking β β - DB-level locking β
+ββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
+```
+
+**Data Flow:**
+1. Server startup reads `database.driver` from configuration
+2. `repo.NewProvider()` creates appropriate repository implementations
+3. Domain services receive repository interfaces (unchanged)
+4. Repositories execute driver-specific SQL
+5. Results returned through common interfaces
+
+## Implementation Design
+
+### Core Interfaces
+
+**Database Driver Interface** (common abstraction for both drivers):
+
+```go
+// engine/infra/sqlite/db.go
+package sqlite
+
+// DB defines the minimal database interface for SQLite operations
+type DB interface {
+ Exec(ctx context.Context, query string, args ...any) (sql.Result, error)
+ Query(ctx context.Context, query string, args ...any) (*sql.Rows, error)
+ QueryRow(ctx context.Context, query string, args ...any) *sql.Row
+ Begin(ctx context.Context) (*sql.Tx, error)
+ Close() error
+}
+
+// Store implements DB using modernc.org/sqlite
+type Store struct {
+ db *sql.DB
+ path string
+}
+```
+
+**Repository Provider** (factory pattern):
+
+```go
+// engine/infra/repo/provider.go
+package repo
+
+type Provider struct {
+ driver string
+}
+
+func NewProvider(ctx context.Context, cfg *config.DatabaseConfig) (*Provider, error) {
+ switch cfg.Driver {
+ case "postgres", "": // default
+ pool := setupPostgresPool(ctx, cfg)
+ return &Provider{
+ driver: "postgres",
+ authRepo: postgres.NewAuthRepo(pool),
+ taskRepo: postgres.NewTaskRepo(pool),
+ workflowRepo: postgres.NewWorkflowRepo(pool),
+ }, nil
+ case "sqlite":
+ db := setupSQLiteDB(ctx, cfg)
+ return &Provider{
+ driver: "sqlite",
+ authRepo: sqlite.NewAuthRepo(db),
+ taskRepo: sqlite.NewTaskRepo(db),
+ workflowRepo: sqlite.NewWorkflowRepo(db),
+ }, nil
+ default:
+ return nil, fmt.Errorf("unsupported database driver: %s", cfg.Driver)
+ }
+}
+```
+
+**SQLite Repository Implementation** (example - Task):
+
+```go
+// engine/infra/sqlite/taskrepo.go
+package sqlite
+
+type TaskRepo struct {
+ db DB
+}
+
+func NewTaskRepo(db DB) *TaskRepo {
+ return &TaskRepo{db: db}
+}
+
+func (r *TaskRepo) UpsertState(ctx context.Context, state *task.State) error {
+ query := `
+ INSERT INTO task_states (
+ task_exec_id, task_id, workflow_exec_id, workflow_id,
+ usage, component, status, execution_type, parent_state_id,
+ input, output, error
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT (task_exec_id) DO UPDATE SET
+ task_id = excluded.task_id,
+ workflow_exec_id = excluded.workflow_exec_id,
+ // ... etc (SQLite uses ? placeholders)
+ `
+ // Convert JSONB fields to JSON strings for SQLite
+ usageJSON, _ := json.Marshal(state.Usage)
+ inputJSON, _ := json.Marshal(state.Input)
+ outputJSON, _ := json.Marshal(state.Output)
+
+ _, err := r.db.Exec(ctx, query,
+ state.TaskExecID, state.TaskID, state.WorkflowExecID,
+ state.WorkflowID, usageJSON, state.Component, state.Status,
+ state.ExecutionType, state.ParentStateID, inputJSON, outputJSON, nil)
+ return err
+}
+```
+
+## Planning Artifacts (Must Be Generated With Tech Spec)
+
+The following artifacts have been generated alongside this Tech Spec:
+
+- **Docs Plan:** `tasks/prd-postgres/_docs.md` - Documentation strategy and page outlines
+- **Examples Plan:** `tasks/prd-postgres/_examples.md` - Example projects and configurations
+- **Tests Plan:** `tasks/prd-postgres/_tests.md` - Test coverage matrix and strategy
+
+### Data Models
+
+**Database Configuration** (extends `pkg/config/config.go`):
+
+```go
+type DatabaseConfig struct {
+ // Driver selection
+ Driver string `koanf:"driver" json:"driver" yaml:"driver" env:"DB_DRIVER" validate:"oneof=postgres sqlite"`
+
+ // PostgreSQL-specific (existing fields, unchanged)
+ ConnString string `koanf:"conn_string" json:"conn_string" yaml:"conn_string"`
+ Host string `koanf:"host" json:"host" yaml:"host"`
+ Port string `koanf:"port" json:"port" yaml:"port"`
+ User string `koanf:"user" json:"user" yaml:"user"`
+ Password string `koanf:"password" json:"password" yaml:"password"`
+ DBName string `koanf:"dbname" json:"dbname" yaml:"dbname"`
+ SSLMode string `koanf:"sslmode" json:"sslmode" yaml:"sslmode"`
+
+ // SQLite-specific (new)
+ Path string `koanf:"path" json:"path" yaml:"path"` // File path or ":memory:"
+
+ // Common settings
+ MaxOpenConns int `koanf:"max_open_conns" json:"max_open_conns" yaml:"max_open_conns"`
+ // ... existing common fields
+}
+```
+
+**Migration File Structure:**
+
+```
+engine/infra/sqlite/migrations/
+βββ 20250603124835_create_workflow_states.sql (SQLite version)
+βββ 20250603124915_create_task_states.sql (SQLite version)
+βββ 20250711163857_create_users.sql (SQLite version)
+βββ 20250711163858_create_api_keys.sql (SQLite version)
+```
+
+### API Endpoints
+
+No new API endpoints required. Database selection is configuration-driven.
+
+## Integration Points
+
+### Vector Database Validation
+
+When using SQLite, the system must validate vector database configuration at startup:
+
+```go
+// engine/infra/server/dependencies.go
+func (s *Server) validateDatabaseConfig(cfg *config.Config) error {
+ if cfg.Database.Driver == "sqlite" {
+ // SQLite cannot use pgvector
+ if len(cfg.Knowledge.VectorDBs) == 0 {
+ return fmt.Errorf(
+ "SQLite requires external vector database. " +
+ "Configure Qdrant, Redis, or Filesystem in knowledge.vector_dbs")
+ }
+
+ // Ensure no pgvector provider configured
+ for _, vdb := range cfg.Knowledge.VectorDBs {
+ if vdb.Provider == "pgvector" {
+ return fmt.Errorf(
+ "pgvector provider incompatible with SQLite. " +
+ "Use Qdrant, Redis, or Filesystem instead")
+ }
+ }
+ }
+ return nil
+}
+```
+
+### Migration System
+
+Use `github.com/pressly/goose/v3` (already in project) which supports both PostgreSQL and SQLite:
+
+```go
+// engine/infra/sqlite/migrations.go
+func ApplyMigrations(ctx context.Context, dbPath string) error {
+ db, err := sql.Open("sqlite", dbPath)
+ if err != nil {
+ return fmt.Errorf("open db for migrations: %w", err)
+ }
+ defer db.Close()
+
+ // Enable foreign keys (required for SQLite)
+ if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
+ return fmt.Errorf("enable foreign keys: %w", err)
+ }
+
+ goose.SetBaseFS(migrationsFS)
+ return goose.Up(db, ".")
+}
+```
+
+## Impact Analysis
+
+| Affected Component | Type of Impact | Description & Risk Level | Required Action |
+|-------------------|---------------|--------------------------|-----------------|
+| `pkg/config` | Config Schema Change | Add `database.driver` field. Low risk - optional field with default. | Add field with "postgres" default |
+| `engine/infra/repo` | Factory Logic | Switch statement for driver selection. Low risk - isolated change. | Add SQLite case to factory |
+| `engine/infra/server` | Startup Logic | Database setup routing. Low risk - early failure path. | Route to SQLite or PostgreSQL setup |
+| `test/integration` | Test Infrastructure | Parameterized tests for both drivers. Medium risk - test duplication. | Create test helpers for multi-driver tests |
+| `test/helpers` | Test Utilities | Database setup helpers. Low risk - additive change. | Add `SetupTestDatabase(driver)` helper |
+| CI/CD Pipeline | Matrix Testing | Run tests against both databases. Medium risk - 2x test time. | Add SQLite test job alongside PostgreSQL |
+| Vector DB Validation | Startup Check | New validation logic. Low risk - clear error messages. | Add validation in server dependencies |
+| Documentation | Multiple Pages | Database selection guide. Low risk - new content. | Create decision matrix, config examples |
+
+**Performance Impact:**
+- SQLite: Acceptable for single-tenant, low-concurrency (5-10 workflows)
+- PostgreSQL: Unchanged, remains high-performance for production
+- Tests: SQLite in-memory mode may be faster for CI/CD
+
+## Testing Approach
+
+### Unit Tests
+
+**Test Organization:**
+```
+engine/infra/sqlite/
+βββ store_test.go # Connection, configuration
+βββ authrepo_test.go # User, API key operations
+βββ taskrepo_test.go # Task CRUD, hierarchy, transactions
+βββ workflowrepo_test.go # Workflow CRUD, state management
+βββ migrations_test.go # Schema creation, foreign keys
+```
+
+**Key Test Scenarios:**
+- **Store Tests:**
+ - Should create database file at specified path
+ - Should use in-memory database for `:memory:` path
+ - Should enable foreign keys on connection
+ - Should handle concurrent connections (pool management)
+
+- **Repository Tests:**
+ - Should implement same interface as PostgreSQL repos
+ - Should pass shared repository test suite
+ - Should handle JSONB β JSON conversion correctly
+ - Should enforce foreign key constraints
+ - Should support upsert operations
+
+**Mock Strategy:**
+- External services only (LLM providers, MCP servers)
+- Database operations use real SQLite (in-memory for speed)
+- Avoid mocking repositories - test real implementations
+
+### Integration Tests
+
+**Test Infrastructure:**
+```go
+// test/helpers/database.go
+func SetupTestDatabase(t *testing.T, driver string) (repo.Provider, func()) {
+ switch driver {
+ case "postgres":
+ return setupPostgresTest(t)
+ case "sqlite":
+ return setupSQLiteTest(t) // Uses :memory:
+ }
+}
+
+// Parameterized integration tests
+func TestWorkflowExecution(t *testing.T) {
+ for _, driver := range []string{"postgres", "sqlite"} {
+ t.Run(driver, func(t *testing.T) {
+ provider, cleanup := SetupTestDatabase(t, driver)
+ defer cleanup()
+
+ // Test workflow execution end-to-end
+ testWorkflowLifecycle(t, provider)
+ })
+ }
+}
+```
+
+**Integration Test Paths:**
+- `test/integration/database/` - Database-specific tests
+ - `sqlite_concurrency_test.go` - Concurrent write handling
+ - `sqlite_transactions_test.go` - Transaction isolation
+ - `sqlite_migrations_test.go` - Schema migrations
+
+- `test/integration/workflow/` - Existing workflow tests, run against both drivers
+- `test/integration/task/` - Existing task tests, run against both drivers
+
+**Test Data:**
+- Use `test/fixtures/workflows/*.yaml` (existing fixtures)
+- SQLite-specific fixtures for edge cases if needed
+
+## Development Sequencing
+
+### Build Order
+
+**1. Foundation Infrastructure (Week 1-2)**
+- Create `engine/infra/sqlite` package structure
+- Implement `Store` (connection management)
+- Add `database.driver` to configuration system
+- Create migration files (SQLite-specific SQL)
+- Set up test infrastructure (helpers, parameterized tests)
+
+**Why First:** Establishes foundation without touching existing code. Low risk, enables parallel development.
+
+**2. Authentication Repository (Week 2)**
+- Implement `sqlite.AuthRepo` (users, API keys)
+- Port migrations: `create_users.sql`, `create_api_keys.sql`
+- Write unit + integration tests
+
+**Why Second:** Simplest repository (basic CRUD, no hierarchy). Validates approach before complex repositories.
+
+**3. Workflow Repository (Week 3)**
+- Implement `sqlite.WorkflowRepo` (workflow state)
+- Port migration: `create_workflow_states.sql`
+- Handle JSON fields (usage, input, output, error)
+- Implement transaction handling
+- Write tests (especially JSON operations)
+
+**Why Third:** Introduces JSON handling patterns, simpler than task repository (no hierarchy).
+
+**4. Task Repository (Week 4-5)**
+- Implement `sqlite.TaskRepo` (task state)
+- Port migration: `create_task_states.sql`
+- Implement hierarchical queries (parent-child relationships)
+- Handle complex CHECK constraints
+- Optimize indexes for performance
+- Write comprehensive tests (hierarchy, locking)
+
+**Why Fourth:** Most complex repository. Builds on patterns from auth and workflow. Critical for workflow execution.
+
+**5. Repository Provider Factory (Week 5)**
+- Update `engine/infra/repo/provider.go` factory
+- Add driver selection logic
+- Implement configuration routing
+- Add vector DB validation for SQLite
+
+**Why Fifth:** Integration point. All repositories must exist first.
+
+**6. Server Integration (Week 6)**
+- Update `engine/infra/server/dependencies.go`
+- Add database setup routing
+- Implement startup validation
+- Add configuration validation
+- Write integration tests (full server startup)
+
+**Why Sixth:** End-to-end integration. Validates entire stack.
+
+**7. Testing & Performance (Week 7-8)**
+- Run full test suite with both drivers
+- Performance benchmarking (SQLite vs PostgreSQL)
+- Concurrency stress tests (SQLite limitations)
+- CI/CD matrix setup
+- Fix bugs and optimize
+
+**Why Seventh:** Quality assurance phase. All functionality must exist first.
+
+**8. Documentation (Week 9)**
+- Write database selection guide
+- Create configuration examples
+- Document performance characteristics
+- Write migration guide
+- Create tutorial/walkthrough
+
+**Why Eighth:** Documentation needs working implementation to test examples.
+
+### Technical Dependencies
+
+**Blocking Dependencies:**
+1. **SQLite Driver Selection:** Decision on `modernc.org/sqlite` vs `go-sqlite3`
+ - **Resolution:** Use `modernc.org/sqlite` (pure Go, no CGO)
+ - **Fallback:** `go-sqlite3` if performance issues
+
+2. **Migration Strategy:** Dual migration files vs shared with conditionals
+ - **Resolution:** Dual migration files (cleaner, database-specific SQL)
+ - **Location:** `engine/infra/sqlite/migrations/` (parallel to postgres)
+
+3. **Test Infrastructure:** In-memory vs file-based SQLite for tests
+ - **Resolution:** In-memory (`:memory:`) for unit tests, file-based for integration
+ - **Benefit:** Faster tests, automatic cleanup
+
+**Non-Blocking (Parallel Work):**
+- Documentation writing (can start alongside implementation)
+- Example project creation (can use PostgreSQL first)
+- Performance benchmarking setup (can prepare harness early)
+
+## Monitoring & Observability
+
+### Metrics
+
+Use existing `engine/infra/monitoring` package. Add driver-specific labels:
+
+```go
+// Database operation metrics (already exists, add driver label)
+database_query_duration_seconds{driver="sqlite", operation="select"}
+database_query_total{driver="sqlite", operation="insert", status="success|error"}
+database_connection_pool_active{driver="sqlite"}
+
+// SQLite-specific metrics
+database_sqlite_wal_size_bytes // Write-Ahead Log size
+database_sqlite_page_count // Total pages
+database_sqlite_file_size_bytes // Database file size
+```
+
+### Logging
+
+Use `logger.FromContext(ctx)` pattern (existing):
+
+```go
+log := logger.FromContext(ctx)
+log.Info("Database initialized",
+ "driver", "sqlite",
+ "path", dbPath,
+ "mode", mode, // "file" or "memory"
+)
+
+log.Warn("SQLite concurrency limitation",
+ "driver", "sqlite",
+ "concurrent_workflows", count,
+ "recommendation", "Use PostgreSQL for >10 concurrent workflows",
+)
+```
+
+### Health Checks
+
+Add SQLite-specific health check (integrate with existing `/health` endpoint):
+
+```go
+func (s *Store) HealthCheck(ctx context.Context) error {
+ // Pragma check
+ var fkEnabled int
+ if err := s.db.QueryRowContext(ctx, "PRAGMA foreign_keys").Scan(&fkEnabled); err != nil {
+ return fmt.Errorf("health check failed: %w", err)
+ }
+ if fkEnabled != 1 {
+ return errors.New("foreign keys not enabled")
+ }
+
+ // Simple query
+ if err := s.db.PingContext(ctx); err != nil {
+ return fmt.Errorf("ping failed: %w", err)
+ }
+
+ return nil
+}
+```
+
+## Technical Considerations
+
+### Key Decisions
+
+**Decision 1: Dual Implementation vs Abstraction Layer**
+
+**Chosen:** Dual Implementation (separate `postgres/` and `sqlite/` packages)
+
+**Rationale:**
+- Allows database-specific optimizations (PostgreSQL keeps pgx features)
+- Cleaner code (no generic abstraction leaking)
+- Lower risk (existing PostgreSQL code unchanged)
+- Easier debugging (clear separation)
+
+**Trade-off:** Code duplication (~80% similar code)
+
+**Alternatives Rejected:**
+- **Abstraction Layer:** Too complex, loses database-specific features, harder to maintain
+- **Shared Implementation:** Least-common-denominator approach, loses PostgreSQL advantages
+
+---
+
+**Decision 2: SQLite Driver Selection**
+
+**Chosen:** `modernc.org/sqlite` (pure Go)
+
+**Rationale:**
+- No CGO required (easier cross-compilation)
+- Fully Go-native (better integration with Go toolchain)
+- Active maintenance
+- Good performance for target use case
+
+**Trade-off:** Slightly slower than `go-sqlite3` (CGO-based)
+
+**Alternatives Considered:**
+- **`github.com/mattn/go-sqlite3`:** More mature, faster, but requires CGO
+- **Fallback Plan:** Switch to `go-sqlite3` if performance issues arise
+
+---
+
+**Decision 3: Vector DB Strategy for SQLite**
+
+**Chosen:** Mandatory external vector DB (Qdrant/Redis/Filesystem)
+
+**Rationale:**
+- SQLite has no native vector support
+- `sqlite-vss` extension too experimental
+- Existing vector DB integrations already available
+- Clean separation of concerns
+
+**Trade-off:** Additional dependency for knowledge features
+
+**Alternatives Rejected:**
+- **sqlite-vss:** Experimental, limited features
+- **Disable Knowledge:** Too limiting for users
+- **Embed pgvector:** Not possible (requires PostgreSQL)
+
+---
+
+**Decision 4: Transaction Locking Strategy**
+
+**Chosen:** Database-level locking (SQLite default) + optimistic locking for specific cases
+
+**Rationale:**
+- SQLite doesn't support row-level locking
+- Target use case (single-tenant, low-concurrency) works with DB-level locking
+- Optimistic locking (version columns) for critical update paths
+
+**Trade-off:** Lower concurrency than PostgreSQL
+
+**Implementation:**
+```go
+// Optimistic locking example
+func (r *TaskRepo) UpdateWithVersion(ctx context.Context, state *task.State) error {
+ query := `
+ UPDATE task_states
+ SET status = ?, version = version + 1
+ WHERE task_exec_id = ? AND version = ?
+ `
+ result, err := r.db.Exec(ctx, query, state.Status, state.TaskExecID, state.Version)
+ if err != nil {
+ return err
+ }
+ rows, _ := result.RowsAffected()
+ if rows == 0 {
+ return ErrConcurrentUpdate // Version mismatch
+ }
+ return nil
+}
+```
+
+### Known Risks
+
+**Risk 1: Concurrency Bottleneck**
+
+**Description:** SQLite serializes writes at database level
+
+**Likelihood:** Medium - Users deploy SQLite in high-concurrency scenarios
+
+**Impact:** High - Performance degradation, locked database
+
+**Mitigation:**
+- Document concurrency limits clearly (5-10 workflows recommended)
+- Add startup warning if SQLite + high-concurrency config detected
+- Provide performance benchmarks in documentation
+- Keep PostgreSQL as recommended production option
+
+---
+
+**Risk 2: SQL Syntax Differences**
+
+**Description:** PostgreSQL and SQLite have different SQL dialects
+
+**Likelihood:** High - Many subtle differences exist
+
+**Impact:** Medium - Query failures, incorrect results
+
+**Examples:**
+- Placeholders: `$1, $2` (PostgreSQL) vs `?, ?` (SQLite)
+- Arrays: `ANY($1::type[])` (PostgreSQL) vs `IN (?, ?, ?)` (SQLite)
+- JSON: `->>`/`->>` (PostgreSQL) vs `json_extract()` (SQLite)
+- Types: `timestamptz` (PostgreSQL) vs `DATETIME` (SQLite)
+
+**Mitigation:**
+- Separate migration files for each database
+- Comprehensive test suite covering all queries
+- SQL syntax compatibility layer for common operations
+- Code review checklist for SQL differences
+
+---
+
+**Risk 3: Migration Divergence**
+
+**Description:** PostgreSQL and SQLite schemas drift over time
+
+**Likelihood:** Medium - New features may only add PostgreSQL migrations
+
+**Impact:** Medium - Features unavailable on SQLite
+
+**Mitigation:**
+- Automated schema comparison tests
+- CI/CD validation of both migration sets
+- Code review process includes both databases
+- Migration template/checklist for new features
+
+### Special Requirements
+
+**Performance Targets (SQLite):**
+- Read latency: <50ms p99 (for single workflow queries)
+- Write latency: <100ms p99 (for state updates)
+- Concurrent workflows: Support 5-10 simultaneous executions
+- Database size: <500MB for typical 1000-workflow history
+
+**Security Considerations:**
+- SQLite file permissions: 0600 (owner read/write only)
+- Path validation: Prevent directory traversal in `database.path`
+- No sensitive data in database file name
+- Backup/export commands must respect file permissions
+
+**Monitoring:**
+- Track database file growth
+- Monitor write contention (lock wait times)
+- Alert on excessive database size (>1GB)
+- Track query performance per driver
+
+### Standards Compliance
+
+**Architecture Compliance:**
+- β
Follows Clean Architecture (domain β application β infrastructure layers)
+- β
Repository pattern with interfaces (Dependency Inversion Principle)
+- β
Factory pattern for provider selection (Open/Closed Principle)
+- β
Context-first configuration (`config.FromContext(ctx)`)
+- β
Context-first logging (`logger.FromContext(ctx)`)
+
+**Go Coding Standards:**
+- β
No global configuration state
+- β
Constructor pattern with nil-safe defaults
+- β
Error wrapping with context (`fmt.Errorf("...: %w", err)`)
+- β
Context propagation throughout
+- β
Resource cleanup with defer
+- β
Test naming: `t.Run("Should ...")`
+
+**Testing Standards:**
+- β
Unit tests for all new code (80%+ coverage)
+- β
Integration tests with real databases (no mocks for DB)
+- β
Parameterized tests for multi-driver scenarios
+- β
Test helpers in `test/helpers/`
+- β
Fixtures in `test/fixtures/`
+
+**Backward Compatibility:**
+- β
No breaking changes (project in alpha, but maintain PostgreSQL compatibility)
+- β
PostgreSQL remains default driver
+- β
Existing configurations work unchanged
+- β
Additive changes only (new `database.driver` field)
+
+## Build vs Buy Analysis
+
+**External Libraries Research:**
+
+| Library | Purpose | License | Adoption | Decision |
+|---------|---------|---------|----------|----------|
+| `modernc.org/sqlite` | SQLite driver | BSD-3 | 1.1k+ stars | β
**ADOPT** |
+| `github.com/mattn/go-sqlite3` | SQLite driver (CGO) | MIT | 7.7k+ stars | π **FALLBACK** |
+| `github.com/pressly/goose/v3` | Migrations | MIT | 6.6k+ stars | β
**EXISTING** |
+| `github.com/Masterminds/squirrel` | Query builder | MIT | 7k+ stars | β
**EXISTING** |
+
+**Rationale:**
+- **`modernc.org/sqlite`:** Pure Go implementation, no CGO, excellent for target use case (development/edge). Active maintenance, good documentation, sufficient performance.
+- **`go-sqlite3`:** Backup option if performance issues. More mature but requires CGO (complicates cross-compilation).
+- **`goose`:** Already in project, supports both PostgreSQL and SQLite. No need to change migration tool.
+- **`squirrel`:** Already in project, database-agnostic query builder. Reuse for both drivers.
+
+**Build Decision:** Implement repository layer in-house. External libraries only for database drivers and migrations. Custom repository implementations allow:
+- Database-specific optimizations
+- Clean interface alignment
+- Full control over query patterns
+- Better testing and debugging
+
+## Libraries Assessment Summary
+
+**Primary Dependency:** `modernc.org/sqlite`
+- **License:** BSD-3-Clause (permissive, commercial-friendly)
+- **Maintenance:** Active (commits within 30 days)
+- **Maturity:** Production-ready, used in many projects
+- **Performance:** Acceptable for target workloads (single-tenant, low-concurrency)
+- **Integration:** Standard `database/sql` interface (drop-in replacement)
+- **Security:** No known CVEs, actively maintained
+- **Footprint:** ~2MB added to binary
+
+**Migration Considerations:**
+- No breaking changes to existing code
+- PostgreSQL driver (`pgx`) remains unchanged
+- New dependency only loaded when SQLite driver selected
+- Pure Go implementation (no CGO) simplifies deployment
+
+**Alternatives Evaluated:**
+- Rejected embedding PostgreSQL (too heavy, defeats purpose)
+- Rejected custom database abstraction (too complex)
+- Rejected database-agnostic ORM (loses control, leaky abstraction)
+
+---
+
+## Appendices
+
+### Appendix A: File Impact Inventory
+
+**PostgreSQL-Specific Files (To Reference/Port):**
+
+```
+engine/infra/postgres/
+βββ authrepo.go (~178 lines) - User/API key operations
+βββ config.go (~24 lines) - PostgreSQL configuration
+βββ doc.go (~10 lines) - Package documentation
+βββ dsn.go (~50 lines) - Connection string builder
+βββ jsonb.go (~50 lines) - JSONB helper functions
+βββ metrics.go (~69 lines) - Pool metrics and observability
+βββ migrations.go (~150 lines) - Migration runner with advisory locks
+βββ migrations/ (9 SQL files) - Schema definitions
+β βββ 20250603124835_create_workflow_states.sql (~34 lines)
+β βββ 20250603124915_create_task_states.sql (~115 lines)
+β βββ 20250711163857_create_users.sql (~17 lines)
+β βββ 20250711163858_create_api_keys.sql (~23 lines)
+β βββ 20250711173300_add_api_key_fingerprint.sql (~27 lines)
+β βββ 20250712120000_add_task_hierarchy_indexes.sql (~20 lines)
+β βββ 20250916090000_add_task_state_query_indexes.sql (~15 lines)
+β βββ 20251012060000_enable_pgvector_extension.sql (~10 lines)
+β βββ 20251016150000_add_task_states_task_exec_idx.sql (~12 lines)
+βββ placeholders.go (~39 lines) - Query placeholder helpers
+βββ queries.go (~50 lines) - Common query constants
+βββ scan.go (~30 lines) - Result scanning helpers
+βββ store.go (~150 lines) - Connection pool management
+βββ taskrepo.go (~500 lines) - Task state repository (COMPLEX)
+βββ workflowrepo.go (~300 lines) - Workflow state repository
+
+engine/infra/repo/
+βββ provider.go (~34 lines) - NEEDS UPDATE: Factory pattern
+
+engine/knowledge/vectordb/
+βββ pgvector.go (~756 lines) - REFERENCE ONLY (cannot port)
+```
+
+**Estimated Lines of Code:**
+- **Core PostgreSQL driver:** ~1,500 lines
+- **Repository implementations:** ~1,000 lines
+- **Migration SQL:** ~300 lines
+- **Total to replicate for SQLite:** ~2,800 lines (excluding pgvector)
+
+**New Files to Create for SQLite:**
+
+```
+engine/infra/sqlite/
+βββ store.go (~150 lines) - Connection management
+βββ authrepo.go (~180 lines) - Port from postgres/authrepo.go
+βββ taskrepo.go (~520 lines) - Port from postgres/taskrepo.go (add SQLite syntax)
+βββ workflowrepo.go (~310 lines) - Port from postgres/workflowrepo.go
+βββ migrations.go (~120 lines) - SQLite migration runner (no advisory locks)
+βββ migrations/ (4 SQL files) - SQLite-specific schema
+β βββ 20250603124835_create_workflow_states.sql
+β βββ 20250603124915_create_task_states.sql
+β βββ 20250711163857_create_users.sql
+β βββ 20250711163858_create_api_keys.sql
+βββ helpers.go (~80 lines) - SQLite-specific utilities
+βββ config.go (~30 lines) - SQLite configuration
+βββ doc.go (~15 lines) - Package documentation
+
+test/helpers/
+βββ database.go (+50 lines) - Add SetupTestDatabase(driver) helper
+```
+
+### Appendix B: Feature Compatibility Matrix (Detailed)
+
+| PostgreSQL Feature | SQLite Equivalent | Migration Complexity | Notes |
+|-------------------|------------------|---------------------|-------|
+| **Data Types** | | | |
+| `text` | `TEXT` | β
Compatible | Same type |
+| `jsonb` | `TEXT` (JSON string) | π‘ Medium | Use `json_extract()`, store as TEXT |
+| `timestamptz` | `DATETIME` or `TEXT` | π‘ Medium | Store as ISO8601 string or Unix timestamp |
+| `bytea` | `BLOB` | β
Compatible | Binary data support |
+| `boolean` | `INTEGER` (0/1) | β
Compatible | SQLite uses 0/1 for booleans |
+| **Placeholders** | | | |
+| `$1, $2, $3` | `?, ?, ?` | π‘ Medium | Replace in all queries |
+| **JSON Operations** | | | |
+| `usage->>'key'` | `json_extract(usage, '$.key')` | π‘ Medium | Different syntax |
+| `jsonb_typeof()` | `json_type()` | β
Compatible | Similar function |
+| `jsonb` operators | JSON functions | π‘ Medium | More verbose in SQLite |
+| **Arrays** | | | |
+| `ANY($1::uuid[])` | `IN (?, ?, ?)` | π΄ High | Expand arrays to multiple placeholders |
+| Array operations | String splitting | π΄ High | SQLite has no native arrays |
+| **Constraints** | | | |
+| `CHECK (...)` | `CHECK (...)` | β
Compatible | Same syntax |
+| `FOREIGN KEY ... CASCADE` | `FOREIGN KEY ... CASCADE` | β
Compatible | Enable with `PRAGMA foreign_keys = ON` |
+| `UNIQUE` | `UNIQUE` | β
Compatible | Same syntax |
+| **Indexes** | | | |
+| B-tree (default) | B-tree (default) | β
Compatible | Same |
+| `GIN (jsonb)` | Expression index | π‘ Medium | Use `CREATE INDEX ... ON table(json_extract(...))` |
+| Partial indexes | Partial indexes | β
Compatible | Same `WHERE` clause syntax |
+| `lower(email)` index | `lower(email)` index | β
Compatible | Same expression syntax |
+| **Transactions** | | | |
+| `BEGIN/COMMIT` | `BEGIN/COMMIT` | β
Compatible | Same |
+| `FOR UPDATE` | β Not supported | π΄ High | Use optimistic locking with version columns |
+| Savepoints | Savepoints | β
Compatible | Same |
+| **Locking** | | | |
+| Row-level locking | Database-level only | π΄ High | Fundamental difference |
+| Advisory locks | β Not available | π‘ Medium | Use file locks for migrations |
+| **Functions** | | | |
+| `now()` | `datetime('now')` or `CURRENT_TIMESTAMP` | π‘ Medium | Different syntax |
+| `GREATEST()` | `max()` | β
Compatible | Similar |
+| **Upsert** | | | |
+| `ON CONFLICT ... DO UPDATE` | `ON CONFLICT ... DO UPDATE` | β
Compatible | Same syntax (SQLite 3.24+) |
+| **Extensions** | | | |
+| pgvector | β None | π΄ **BLOCKER** | Require external vector DB |
+
+**Legend:**
+- β
**Compatible:** Direct port, minimal changes
+- π‘ **Medium:** Requires syntax changes but straightforward
+- π΄ **High:** Significant changes or workarounds needed
+
+### Appendix C: SQL Schema Examples
+
+**PostgreSQL Migration (existing):**
+
+```sql
+-- engine/infra/postgres/migrations/20250603124835_create_workflow_states.sql
+CREATE TABLE IF NOT EXISTS workflow_states (
+ workflow_exec_id text NOT NULL PRIMARY KEY,
+ workflow_id text NOT NULL,
+ status text NOT NULL,
+ usage jsonb, -- PostgreSQL JSONB type
+ input jsonb,
+ output jsonb,
+ error jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(), -- PostgreSQL timestamptz
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+
+ALTER TABLE workflow_states
+ ADD CONSTRAINT chk_workflow_states_usage_json
+ CHECK (usage IS NULL OR jsonb_typeof(usage) = 'array'); -- PostgreSQL function
+
+CREATE INDEX idx_workflow_states_status ON workflow_states (status);
+```
+
+**SQLite Migration (to create):**
+
+```sql
+-- engine/infra/sqlite/migrations/20250603124835_create_workflow_states.sql
+CREATE TABLE IF NOT EXISTS workflow_states (
+ workflow_exec_id TEXT NOT NULL PRIMARY KEY,
+ workflow_id TEXT NOT NULL,
+ status TEXT NOT NULL,
+ usage TEXT, -- Store JSON as TEXT
+ input TEXT,
+ output TEXT,
+ error TEXT,
+ created_at TEXT NOT NULL DEFAULT (datetime('now')), -- SQLite datetime
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
+);
+
+-- Check constraint using SQLite's json_type()
+-- Note: SQLite doesn't have ALTER TABLE ADD CONSTRAINT, so include in CREATE TABLE
+-- or create as separate check:
+CREATE TABLE IF NOT EXISTS workflow_states (
+ -- ... (as above)
+ CHECK (usage IS NULL OR json_type(usage) = 'array') -- SQLite function
+);
+
+CREATE INDEX idx_workflow_states_status ON workflow_states (status);
+```
+
+**PostgreSQL Task States (complex):**
+
+```sql
+-- engine/infra/postgres/migrations/20250603124915_create_task_states.sql
+CREATE TABLE IF NOT EXISTS task_states (
+ task_exec_id text NOT NULL PRIMARY KEY,
+ workflow_exec_id text NOT NULL,
+ parent_state_id text,
+ usage jsonb,
+ input jsonb,
+ output jsonb,
+ error jsonb,
+ -- ... other fields
+
+ -- Foreign keys with CASCADE
+ CONSTRAINT fk_workflow
+ FOREIGN KEY (workflow_exec_id)
+ REFERENCES workflow_states (workflow_exec_id)
+ ON DELETE CASCADE,
+
+ CONSTRAINT fk_parent_task
+ FOREIGN KEY (parent_state_id)
+ REFERENCES task_states (task_exec_id)
+ ON DELETE CASCADE,
+
+ -- Complex CHECK constraint
+ CONSTRAINT chk_execution_type_consistency
+ CHECK (
+ (execution_type = 'basic' AND (
+ (agent_id IS NOT NULL AND action_id IS NOT NULL) OR
+ (tool_id IS NOT NULL AND agent_id IS NULL)
+ )) OR
+ (execution_type = 'router' AND agent_id IS NULL)
+ -- ... more conditions
+ )
+);
+```
+
+**SQLite Task States (ported):**
+
+```sql
+-- engine/infra/sqlite/migrations/20250603124915_create_task_states.sql
+-- Note: Enable foreign keys first with PRAGMA
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE IF NOT EXISTS task_states (
+ task_exec_id TEXT NOT NULL PRIMARY KEY,
+ workflow_exec_id TEXT NOT NULL,
+ parent_state_id TEXT,
+ usage TEXT, -- JSON as TEXT
+ input TEXT,
+ output TEXT,
+ error TEXT,
+ -- ... other fields
+
+ -- Foreign keys work the same in SQLite (when enabled)
+ FOREIGN KEY (workflow_exec_id)
+ REFERENCES workflow_states (workflow_exec_id)
+ ON DELETE CASCADE,
+
+ FOREIGN KEY (parent_state_id)
+ REFERENCES task_states (task_exec_id)
+ ON DELETE CASCADE,
+
+ -- CHECK constraints work the same
+ CHECK (
+ (execution_type = 'basic' AND (
+ (agent_id IS NOT NULL AND action_id IS NOT NULL) OR
+ (tool_id IS NOT NULL AND agent_id IS NULL)
+ )) OR
+ (execution_type = 'router' AND agent_id IS NULL)
+ -- ... same conditions
+ )
+);
+```
+
+### Appendix D: Performance Characteristics
+
+**Comparative Performance Analysis:**
+
+| Operation | PostgreSQL | SQLite | Notes |
+|-----------|-----------|--------|-------|
+| **Read Performance** | βββββ (Excellent) | ββββ (Very Good) | Both I/O-bound; PostgreSQL has better caching |
+| **Write Performance** | βββββ (Excellent) | βββ (Good) | SQLite write serialization limits throughput |
+| **Concurrent Writes** | βββββ (25+ workflows) | ββ (5-10 workflows) | SQLite database-level locking |
+| **Concurrent Reads** | βββββ (Unlimited) | βββββ (Unlimited) | Both excellent for read-heavy workloads |
+| **Complex Queries** | βββββ (Excellent) | ββββ (Very Good) | PostgreSQL has query planner advantages |
+| **Vector Search** | βββββ (pgvector built-in) | β (External DB required) | **Critical difference** |
+| **JSON Operations** | βββββ (JSONB native) | ββββ (JSON1 extension) | PostgreSQL more feature-rich |
+| **Deployment** | βββ (Separate service) | βββββ (Single file) | SQLite much simpler |
+| **Horizontal Scaling** | βββββ (Excellent) | β (Not designed for this) | PostgreSQL for distributed systems |
+| **Backup/Recovery** | ββββ (pg_dump, WAL) | βββββ (File copy) | SQLite simpler but less granular |
+| **Transaction Safety** | βββββ (ACID) | βββββ (ACID) | Both fully ACID-compliant |
+| **Memory Footprint** | βββ (Higher) | βββββ (Minimal) | SQLite excellent for constrained environments |
+
+**Performance Targets (SQLite):**
+
+```
+Latency Targets:
+- Read (single workflow): p50 < 10ms, p99 < 50ms
+- Write (state update): p50 < 20ms, p99 < 100ms
+- Hierarchical query (tasks): p50 < 30ms, p99 < 150ms
+
+Throughput Targets:
+- Concurrent workflows: 5-10 simultaneous (recommended)
+- Workflow starts/hour: ~500 (moderate load)
+- State updates/second: ~20-30 (write-heavy)
+
+Storage Targets:
+- Database file size: <500MB for 1000 workflows
+- WAL size: <50MB typical
+- Growth rate: ~400KB per workflow (avg)
+```
+
+### Appendix E: Dependencies
+
+**Required Dependencies (New):**
+
+```go
+// go.mod additions
+require (
+ modernc.org/sqlite v1.31.1 // Pure Go SQLite driver (primary choice)
+ // OR
+ // github.com/mattn/go-sqlite3 v1.14.22 // CGO-based (fallback)
+)
+```
+
+**Existing Dependencies (Reused):**
+
+```go
+// Already in go.mod
+github.com/pressly/goose/v3 v3.20.0 // Migrations (supports both DBs)
+github.com/Masterminds/squirrel v1.5.4 // Query builder (DB-agnostic)
+github.com/jackc/pgx/v5 v5.6.0 // PostgreSQL (keep existing)
+github.com/jackc/pgx/v5/pgxpool v5.6.0 // PostgreSQL pool (keep existing)
+```
+
+**Development Dependencies:**
+
+```go
+// Testing
+github.com/stretchr/testify v1.9.0 // Assertions (existing)
+github.com/testcontainers/testcontainers-go v0.31.0 // PostgreSQL containers (existing)
+```
+
+**Binary Size Impact:**
+
+```
+Current binary (with PostgreSQL): ~45MB
+After adding SQLite (pure Go): ~47MB (+2MB)
+After adding SQLite (CGO): ~46MB (+1MB)
+```
+
+### Appendix F: Key Differences Checklist
+
+**SQL Syntax Differences to Handle:**
+
+```
+PostgreSQL β SQLite Conversions:
+
+1. Placeholders:
+ - PG: $1, $2, $3 β SQLite: ?, ?, ?
+
+2. Data Types:
+ - PG: timestamptz β SQLite: TEXT (ISO8601) or INTEGER (unix)
+ - PG: jsonb β SQLite: TEXT (JSON string)
+ - PG: bytea β SQLite: BLOB
+
+3. JSON Operations:
+ - PG: usage->>'key' β SQLite: json_extract(usage, '$.key')
+ - PG: jsonb_typeof() β SQLite: json_type()
+ - PG: usage @> '{"k":"v"}' β SQLite: (parse and compare)
+
+4. Array Operations:
+ - PG: ANY($1::uuid[]) β SQLite: IN (?, ?, ...) with expanded params
+ - PG: array_agg() β SQLite: group_concat() or JSON
+
+5. Date Functions:
+ - PG: now() β SQLite: datetime('now') or CURRENT_TIMESTAMP
+ - PG: EXTRACT(YEAR ...) β SQLite: strftime('%Y', ...)
+
+6. String Functions:
+ - PG: lower(), upper() β SQLite: same
+ - PG: concat() β SQLite: || operator or concat()
+
+7. Aggregates:
+ - PG: GREATEST() β SQLite: max()
+ - PG: LEAST() β SQLite: min()
+
+8. Indexes:
+ - PG: GIN (jsonb_col) β SQLite: Expression index on json_extract()
+ - PG: Partial indexes β SQLite: Same (WHERE clause)
+
+9. Constraints:
+ - PG: CHECK (inline) β SQLite: CHECK (inline) - same
+ - PG: Foreign keys β SQLite: Same but need PRAGMA foreign_keys = ON
+
+10. Locking:
+ - PG: FOR UPDATE β SQLite: Not supported (use optimistic locking)
+ - PG: Advisory locks β SQLite: Not supported (use file locks)
+```
+
+### Appendix G: Migration Effort Breakdown
+
+**Detailed Task Breakdown:**
+
+| Task | Subtasks | Estimated Hours | Complexity |
+|------|----------|----------------|------------|
+| **Phase 1: Foundation** | | **80-120 hours** | |
+| SQLite store setup | Connection, pool, health checks | 8-12h | Low |
+| Configuration | Add driver field, validation | 4-6h | Low |
+| Migration system | Port goose setup, PRAGMA handling | 8-12h | Low |
+| Test infrastructure | Helpers, parameterized tests | 12-16h | Medium |
+| **Phase 2: Auth Repo** | | **40-60 hours** | |
+| Port authrepo.go | Users, API keys | 16-24h | Low |
+| Port migrations | create_users, create_api_keys | 4-6h | Low |
+| Unit tests | All CRUD operations | 12-16h | Low |
+| Integration tests | Full auth flow | 8-12h | Medium |
+| **Phase 3: Workflow Repo** | | **60-80 hours** | |
+| Port workflowrepo.go | State management | 20-28h | Medium |
+| JSON handling | JSONB β TEXT conversion | 8-12h | Medium |
+| Port migrations | create_workflow_states | 4-6h | Low |
+| Unit tests | CRUD + JSON ops | 16-20h | Medium |
+| Integration tests | Full workflow lifecycle | 12-16h | Medium |
+| **Phase 4: Task Repo** | | **100-140 hours** | |
+| Port taskrepo.go | State management | 32-44h | **High** |
+| Hierarchical queries | Parent-child relationships | 20-28h | **High** |
+| JSON handling | Complex JSONB operations | 12-16h | Medium |
+| Array operations | ANY() β IN() conversion | 8-12h | Medium |
+| Port migrations | create_task_states (complex) | 8-12h | Medium |
+| Unit tests | CRUD + hierarchy + JSON | 24-32h | High |
+| Integration tests | Full task execution | 16-20h | High |
+| **Phase 5: Integration** | | **40-60 hours** | |
+| Provider factory | Driver selection logic | 8-12h | Low |
+| Server integration | Startup routing | 8-12h | Medium |
+| Vector DB validation | Startup checks | 4-6h | Low |
+| End-to-end tests | Full server with both drivers | 16-24h | Medium |
+| Bug fixes | Integration issues | 8-12h | Variable |
+| **Phase 6: Performance** | | **60-80 hours** | |
+| Benchmarking | Performance tests | 16-20h | Medium |
+| Optimization | Query tuning, indexes | 20-28h | High |
+| Concurrency testing | Stress tests, lock handling | 16-20h | High |
+| CI/CD setup | Matrix testing | 8-12h | Medium |
+| **Phase 7: Documentation** | | **40-60 hours** | |
+| Technical docs | Decision guide, config | 16-20h | Low |
+| Examples | 6 example projects | 16-24h | Medium |
+| Migration guide | PostgreSQL β SQLite | 8-12h | Medium |
+| **Total** | | **420-600 hours** | **8-12 weeks** |
+
+**Risk Contingency:** Add 20% buffer (84-120 hours) for unforeseen issues, totaling **504-720 hours (10-14 weeks)**.
+
+---
+
+**Document Version:** 1.1
+**Date:** 2025-01-27
+**Author:** AI Analysis
+**Status:** Technical Specification Complete (Enhanced with Appendices)
diff --git a/tasks/prd-postgres/_tests.md b/tasks/prd-postgres/_tests.md
new file mode 100644
index 00000000..92c97038
--- /dev/null
+++ b/tasks/prd-postgres/_tests.md
@@ -0,0 +1,727 @@
+# Tests Plan: SQLite Database Backend Support
+
+## Guiding Principles
+
+- Follow `.cursor/rules/test-standards.mdc` strictly
+- Use `t.Run("Should ...")` pattern for all test cases
+- Use testify assertions for clarity
+- Context from `t.Context()` (never `context.Background()`)
+- Real databases (no mocks for database operations)
+- Parameterized tests for multi-driver scenarios
+
+## Coverage Matrix
+
+| PRD Requirement | Test Files | Coverage Type |
+|----------------|------------|---------------|
+| FR-1: Driver Selection | `pkg/config/config_test.go` | Unit |
+| FR-2: SQLite Driver | `engine/infra/sqlite/*_test.go` | Unit + Integration |
+| FR-3: Database Config | `pkg/config/loader_test.go` | Unit |
+| FR-4: Migrations | `engine/infra/sqlite/migrations_test.go` | Integration |
+| FR-5: Vector DB Validation | `engine/infra/server/dependencies_test.go` | Unit |
+| NFR-1: Performance | `test/integration/database/performance_test.go` | Performance |
+| NFR-2: Compatibility | `test/integration/database/compatibility_test.go` | Integration |
+| NFR-3: Test Coverage | All test files | Coverage Report |
+
+## Unit Tests
+
+### `pkg/config/config_test.go` (UPDATE)
+
+**Add tests for database driver selection:**
+
+- `TestDatabaseConfig/Should_default_to_postgres_when_driver_empty`
+- `TestDatabaseConfig/Should_accept_postgres_driver_explicitly`
+- `TestDatabaseConfig/Should_accept_sqlite_driver`
+- `TestDatabaseConfig/Should_reject_invalid_driver`
+- `TestDatabaseConfig/Should_require_path_for_sqlite`
+- `TestDatabaseConfig/Should_require_connection_params_for_postgres`
+- `TestDatabaseConfig/Should_validate_sqlite_path_format`
+
+**Example:**
+```go
+func TestDatabaseConfig(t *testing.T) {
+ t.Run("Should default to postgres when driver empty", func(t *testing.T) {
+ cfg := &config.DatabaseConfig{
+ Host: "localhost",
+ User: "test",
+ }
+ err := cfg.Validate()
+ assert.NoError(t, err)
+ assert.Equal(t, "postgres", cfg.Driver) // default
+ })
+
+ t.Run("Should accept sqlite driver", func(t *testing.T) {
+ cfg := &config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: "./test.db",
+ }
+ err := cfg.Validate()
+ assert.NoError(t, err)
+ })
+
+ t.Run("Should reject invalid driver", func(t *testing.T) {
+ cfg := &config.DatabaseConfig{
+ Driver: "mysql",
+ }
+ err := cfg.Validate()
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "unsupported driver")
+ })
+}
+```
+
+### `engine/infra/sqlite/store_test.go` (NEW)
+
+**Test SQLite connection management:**
+
+- `TestStore/Should_create_file_database_at_specified_path`
+- `TestStore/Should_create_in_memory_database_for_memory_path`
+- `TestStore/Should_enable_foreign_keys_on_connection`
+- `TestStore/Should_handle_concurrent_connections`
+- `TestStore/Should_return_error_for_invalid_path`
+- `TestStore/Should_close_cleanly`
+- `TestStore/Should_perform_health_check_successfully`
+
+### `engine/infra/sqlite/authrepo_test.go` (NEW)
+
+**Test user and API key operations:**
+
+- `TestAuthRepo/Should_create_user_successfully`
+- `TestAuthRepo/Should_get_user_by_id`
+- `TestAuthRepo/Should_get_user_by_email_case_insensitive`
+- `TestAuthRepo/Should_return_error_for_duplicate_email`
+- `TestAuthRepo/Should_list_all_users`
+- `TestAuthRepo/Should_update_user`
+- `TestAuthRepo/Should_delete_user`
+- `TestAuthRepo/Should_create_api_key`
+- `TestAuthRepo/Should_cascade_delete_api_keys_when_user_deleted`
+- `TestAuthRepo/Should_enforce_foreign_key_constraint`
+
+### `engine/infra/sqlite/taskrepo_test.go` (NEW)
+
+**Test task state persistence and hierarchy:**
+
+- `TestTaskRepo/Should_upsert_task_state`
+- `TestTaskRepo/Should_get_task_state_by_id`
+- `TestTaskRepo/Should_list_tasks_by_workflow`
+- `TestTaskRepo/Should_list_tasks_by_status`
+- `TestTaskRepo/Should_list_children_of_parent_task`
+- `TestTaskRepo/Should_get_task_tree_recursively`
+- `TestTaskRepo/Should_handle_jsonb_fields_correctly`
+- `TestTaskRepo/Should_enforce_foreign_key_to_workflow`
+- `TestTaskRepo/Should_cascade_delete_children_when_parent_deleted`
+- `TestTaskRepo/Should_execute_transaction_atomically`
+- `TestTaskRepo/Should_handle_concurrent_updates`
+
+**Example:**
+```go
+func TestTaskRepo(t *testing.T) {
+ db := setupTestSQLite(t)
+ repo := sqlite.NewTaskRepo(db)
+
+ t.Run("Should upsert task state", func(t *testing.T) {
+ ctx := t.Context()
+ state := &task.State{
+ TaskExecID: core.NewID(),
+ TaskID: "test-task",
+ WorkflowExecID: setupTestWorkflow(t, db),
+ Status: core.StatusRunning,
+ Component: "task",
+ }
+
+ err := repo.UpsertState(ctx, state)
+ assert.NoError(t, err)
+
+ // Verify
+ retrieved, err := repo.GetState(ctx, state.TaskExecID)
+ assert.NoError(t, err)
+ assert.Equal(t, state.TaskID, retrieved.TaskID)
+ })
+
+ t.Run("Should handle jsonb fields correctly", func(t *testing.T) {
+ ctx := t.Context()
+ input := map[string]any{"key": "value"}
+ state := &task.State{
+ TaskExecID: core.NewID(),
+ WorkflowExecID: setupTestWorkflow(t, db),
+ Input: &core.Input{Data: input},
+ }
+
+ err := repo.UpsertState(ctx, state)
+ assert.NoError(t, err)
+
+ retrieved, err := repo.GetState(ctx, state.TaskExecID)
+ assert.NoError(t, err)
+ assert.Equal(t, input, retrieved.Input.Data)
+ })
+}
+```
+
+### `engine/infra/sqlite/workflowrepo_test.go` (NEW)
+
+**Test workflow state operations:**
+
+- `TestWorkflowRepo/Should_upsert_workflow_state`
+- `TestWorkflowRepo/Should_get_workflow_state_by_exec_id`
+- `TestWorkflowRepo/Should_list_workflows_by_status`
+- `TestWorkflowRepo/Should_update_workflow_status`
+- `TestWorkflowRepo/Should_complete_workflow_with_output`
+- `TestWorkflowRepo/Should_merge_usage_statistics`
+- `TestWorkflowRepo/Should_handle_jsonb_usage_field`
+
+### `engine/infra/sqlite/migrations_test.go` (NEW)
+
+**Test migration system:**
+
+- `TestMigrations/Should_apply_all_migrations_successfully`
+- `TestMigrations/Should_create_all_required_tables`
+- `TestMigrations/Should_create_all_indexes`
+- `TestMigrations/Should_enforce_foreign_keys`
+- `TestMigrations/Should_enforce_check_constraints`
+- `TestMigrations/Should_rollback_migrations`
+- `TestMigrations/Should_be_idempotent`
+
+### `engine/infra/repo/provider_test.go` (UPDATE)
+
+**Test factory pattern:**
+
+- `TestProvider/Should_create_postgres_provider_by_default`
+- `TestProvider/Should_create_postgres_provider_explicitly`
+- `TestProvider/Should_create_sqlite_provider`
+- `TestProvider/Should_return_error_for_invalid_driver`
+- `TestProvider/Should_configure_postgres_repositories_correctly`
+- `TestProvider/Should_configure_sqlite_repositories_correctly`
+
+### `engine/infra/server/dependencies_test.go` (UPDATE)
+
+**Test vector DB validation:**
+
+- `TestValidateDatabaseConfig/Should_pass_postgres_with_pgvector`
+- `TestValidateDatabaseConfig/Should_pass_postgres_without_vector_db`
+- `TestValidateDatabaseConfig/Should_pass_sqlite_with_qdrant`
+- `TestValidateDatabaseConfig/Should_pass_sqlite_with_redis`
+- `TestValidateDatabaseConfig/Should_pass_sqlite_with_filesystem`
+- `TestValidateDatabaseConfig/Should_fail_sqlite_with_pgvector`
+- `TestValidateDatabaseConfig/Should_fail_sqlite_without_vector_db_when_knowledge_enabled`
+
+**Example:**
+```go
+func TestValidateDatabaseConfig(t *testing.T) {
+ t.Run("Should fail sqlite with pgvector", func(t *testing.T) {
+ cfg := &config.Config{
+ Database: config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: "./test.db",
+ },
+ Knowledge: config.KnowledgeConfig{
+ VectorDBs: []config.VectorDBConfig{
+ {Provider: "pgvector"},
+ },
+ },
+ }
+
+ err := validateDatabaseConfig(cfg)
+ assert.Error(t, err)
+ assert.Contains(t, err.Error(), "pgvector")
+ assert.Contains(t, err.Error(), "incompatible with SQLite")
+ })
+}
+```
+
+## Integration Tests
+
+### `test/integration/database/multi_driver_test.go` (NEW)
+
+**Parameterized tests for both drivers:**
+
+```go
+func TestMultiDriver_WorkflowExecution(t *testing.T) {
+ drivers := []string{"postgres", "sqlite"}
+
+ for _, driver := range drivers {
+ t.Run(driver, func(t *testing.T) {
+ provider, cleanup := setupTestDatabase(t, driver)
+ defer cleanup()
+
+ t.Run("Should execute workflow end-to-end", func(t *testing.T) {
+ testWorkflowExecution(t, provider)
+ })
+
+ t.Run("Should persist task hierarchy", func(t *testing.T) {
+ testTaskHierarchy(t, provider)
+ })
+
+ t.Run("Should handle concurrent workflows", func(t *testing.T) {
+ testConcurrentWorkflows(t, provider, 5) // Conservative for SQLite
+ })
+ })
+ }
+}
+```
+
+**Test Cases:**
+- `TestMultiDriver_WorkflowExecution/Should_execute_workflow_end_to_end`
+- `TestMultiDriver_WorkflowExecution/Should_persist_task_hierarchy`
+- `TestMultiDriver_WorkflowExecution/Should_handle_concurrent_workflows`
+- `TestMultiDriver_Authentication/Should_authenticate_user_with_api_key`
+- `TestMultiDriver_Transactions/Should_rollback_on_error`
+- `TestMultiDriver_Transactions/Should_commit_on_success`
+
+### `test/integration/database/sqlite_specific_test.go` (NEW)
+
+**SQLite-specific behavior tests:**
+
+- `TestSQLite/Should_handle_database_locked_gracefully`
+- `TestSQLite/Should_support_in_memory_mode`
+- `TestSQLite/Should_create_database_file_if_not_exists`
+- `TestSQLite/Should_enforce_foreign_keys`
+- `TestSQLite/Should_handle_concurrent_reads`
+- `TestSQLite/Should_serialize_concurrent_writes`
+- `TestSQLite/Should_work_with_wal_mode`
+
+### `test/integration/database/performance_test.go` (NEW)
+
+**Performance benchmarks:**
+
+```go
+func BenchmarkDatabase_ReadOperations(b *testing.B) {
+ drivers := []string{"postgres", "sqlite"}
+
+ for _, driver := range drivers {
+ b.Run(driver, func(b *testing.B) {
+ provider, cleanup := setupBenchDatabase(b, driver)
+ defer cleanup()
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _, err := provider.NewWorkflowRepo().GetState(
+ context.Background(),
+ testWorkflowID,
+ )
+ if err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+ }
+}
+```
+
+**Benchmarks:**
+- `BenchmarkDatabase_ReadOperations`
+- `BenchmarkDatabase_WriteOperations`
+- `BenchmarkDatabase_TransactionOperations`
+- `BenchmarkDatabase_HierarchicalQueries`
+
+### `test/integration/database/compatibility_test.go` (NEW)
+
+**Test backward compatibility:**
+
+- `TestCompatibility/Should_work_with_existing_postgres_config`
+- `TestCompatibility/Should_not_break_existing_workflows`
+- `TestCompatibility/Should_migrate_schema_without_data_loss`
+
+## Fixtures & Testdata
+
+### New Fixtures
+
+**`test/fixtures/database/sqlite-config.yaml`:**
+```yaml
+database:
+ driver: sqlite
+ path: ":memory:"
+```
+
+**`test/fixtures/database/postgres-config.yaml`:**
+```yaml
+database:
+ driver: postgres
+ host: localhost
+ port: 5432
+ user: test
+ password: test
+ dbname: test_compozy
+```
+
+**`test/fixtures/database/sqlite-qdrant-config.yaml`:**
+```yaml
+database:
+ driver: sqlite
+ path: "./test.db"
+
+knowledge:
+ vector_dbs:
+ - id: test
+ provider: qdrant
+ url: http://localhost:6333
+```
+
+### Test Helpers
+
+**`test/helpers/database.go` (UPDATE):**
+
+```go
+// SetupTestDatabase creates a test database for the specified driver
+func SetupTestDatabase(t *testing.T, driver string) (*repo.Provider, func()) {
+ t.Helper()
+
+ switch driver {
+ case "postgres":
+ return setupPostgresTest(t)
+ case "sqlite":
+ return setupSQLiteTest(t)
+ default:
+ t.Fatalf("unsupported driver: %s", driver)
+ return nil, nil
+ }
+}
+
+func setupSQLiteTest(t *testing.T) (*repo.Provider, func()) {
+ t.Helper()
+
+ // Use in-memory SQLite for fast tests
+ cfg := &config.DatabaseConfig{
+ Driver: "sqlite",
+ Path: ":memory:",
+ }
+
+ db, err := sqlite.NewStore(t.Context(), cfg)
+ require.NoError(t, err)
+
+ // Apply migrations
+ err = sqlite.ApplyMigrations(t.Context(), ":memory:")
+ require.NoError(t, err)
+
+ provider := repo.NewProvider(db.DB())
+
+ cleanup := func() {
+ db.Close(t.Context())
+ }
+
+ return provider, cleanup
+}
+```
+
+## Mocks & Stubs
+
+**Minimal mocking strategy:**
+- β No mocks for database operations (use real databases)
+- β
Mock external LLM providers
+- β
Mock external MCP servers
+- β
Mock external vector DBs (optional, if Qdrant/Redis not available)
+
+**Mock Files:**
+- No new mocks required (database operations test against real DBs)
+
+## API Contract Assertions (if applicable)
+
+No API changes - database selection is configuration-driven.
+
+## Observability Assertions
+
+### Metrics Tests
+
+**`engine/infra/monitoring/database_test.go` (UPDATE):**
+
+- `TestDatabaseMetrics/Should_emit_query_duration_with_driver_label`
+- `TestDatabaseMetrics/Should_emit_query_count_with_driver_label`
+- `TestDatabaseMetrics/Should_emit_connection_pool_metrics`
+- `TestDatabaseMetrics/Should_emit_sqlite_specific_metrics`
+
+**Example:**
+```go
+func TestDatabaseMetrics(t *testing.T) {
+ t.Run("Should emit query duration with driver label", func(t *testing.T) {
+ // Setup metric collector
+ registry := prometheus.NewRegistry()
+
+ // Execute query
+ provider, cleanup := setupTestDatabase(t, "sqlite")
+ defer cleanup()
+
+ _, err := provider.NewWorkflowRepo().GetState(t.Context(), testID)
+ require.NoError(t, err)
+
+ // Assert metric
+ metrics, _ := registry.Gather()
+ found := false
+ for _, mf := range metrics {
+ if *mf.Name == "database_query_duration_seconds" {
+ for _, m := range mf.Metric {
+ for _, label := range m.Label {
+ if *label.Name == "driver" && *label.Value == "sqlite" {
+ found = true
+ }
+ }
+ }
+ }
+ }
+ assert.True(t, found, "metric not found")
+ })
+}
+```
+
+### Logging Tests
+
+**`engine/infra/sqlite/store_test.go` (extend):**
+
+- `TestStore/Should_log_initialization_with_driver_label`
+- `TestStore/Should_log_warnings_for_concurrency_limits`
+- `TestStore/Should_log_errors_with_context`
+
+## Performance & Limits
+
+### Performance Tests
+
+**`test/integration/database/performance_test.go` (detailed):**
+
+```go
+func TestSQLitePerformance(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping performance test in short mode")
+ }
+
+ t.Run("Should handle 10 concurrent workflows", func(t *testing.T) {
+ provider, cleanup := setupTestDatabase(t, "sqlite")
+ defer cleanup()
+
+ ctx := t.Context()
+ concurrency := 10
+ errors := make(chan error, concurrency)
+
+ start := time.Now()
+
+ for i := 0; i < concurrency; i++ {
+ go func(id int) {
+ workflow := createTestWorkflow(id)
+ errors <- provider.NewWorkflowRepo().UpsertState(ctx, workflow)
+ }(i)
+ }
+
+ // Collect errors
+ for i := 0; i < concurrency; i++ {
+ err := <-errors
+ assert.NoError(t, err)
+ }
+
+ duration := time.Since(start)
+ t.Logf("10 concurrent workflows completed in %v", duration)
+
+ // Assert performance target
+ assert.Less(t, duration, 5*time.Second, "should complete within 5s")
+ })
+
+ t.Run("Should maintain p99 latency under 100ms", func(t *testing.T) {
+ provider, cleanup := setupTestDatabase(t, "sqlite")
+ defer cleanup()
+
+ latencies := make([]time.Duration, 100)
+
+ for i := 0; i < 100; i++ {
+ start := time.Now()
+ _, err := provider.NewWorkflowRepo().GetState(t.Context(), testID)
+ require.NoError(t, err)
+ latencies[i] = time.Since(start)
+ }
+
+ // Calculate p99
+ sort.Slice(latencies, func(i, j int) bool {
+ return latencies[i] < latencies[j]
+ })
+ p99 := latencies[98]
+
+ t.Logf("p99 latency: %v", p99)
+ assert.Less(t, p99, 100*time.Millisecond)
+ })
+}
+```
+
+**Performance Targets:**
+- Read operations: p99 < 50ms
+- Write operations: p99 < 100ms
+- 10 concurrent workflows: Complete in < 5s
+- Database file size: < 10MB for 100 workflows
+
+### Limit Tests
+
+**`test/integration/database/limits_test.go` (NEW):**
+
+- `TestLimits/Should_handle_1000_workflows`
+- `TestLimits/Should_handle_deep_task_hierarchy`
+- `TestLimits/Should_handle_large_jsonb_fields`
+
+## CLI Tests (Goldens)
+
+### Golden Tests
+
+**`cli/cmd/start_test.go` (UPDATE):**
+
+- `TestStart/Should_show_sqlite_in_startup_logs`
+- `TestStart/Should_show_postgres_in_startup_logs`
+- `TestStart/Should_warn_if_sqlite_with_high_concurrency`
+
+**Golden Files:**
+- `cli/cmd/testdata/start-sqlite.golden`
+- `cli/cmd/testdata/start-postgres.golden`
+
+**Example:**
+```go
+func TestStartCommand(t *testing.T) {
+ t.Run("Should show sqlite in startup logs", func(t *testing.T) {
+ output := captureOutput(func() {
+ cmd := startCommand()
+ cmd.SetArgs([]string{
+ "--db-driver=sqlite",
+ "--db-path=:memory:",
+ })
+ err := cmd.Execute()
+ require.NoError(t, err)
+ })
+
+ golden.Assert(t, output, "start-sqlite.golden")
+ assert.Contains(t, output, "driver=sqlite")
+ assert.Contains(t, output, "path=:memory:")
+ })
+}
+```
+
+## CI/CD Configuration
+
+### GitHub Actions Matrix
+
+**`.github/workflows/test.yml` (UPDATE):**
+
+```yaml
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ database: [postgres, sqlite]
+
+ services:
+ postgres:
+ image: pgvector/pgvector:pg16
+ if: matrix.database == 'postgres'
+ env:
+ POSTGRES_PASSWORD: test
+ POSTGRES_USER: test
+ POSTGRES_DB: test_compozy
+ ports:
+ - 5432:5432
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-go@v4
+ with:
+ go-version: "1.25.2"
+
+ - name: Run tests (PostgreSQL)
+ if: matrix.database == 'postgres'
+ env:
+ DB_DRIVER: postgres
+ DB_HOST: localhost
+ DB_PORT: 5432
+ DB_USER: test
+ DB_PASSWORD: test
+ DB_NAME: test_compozy
+ run: make test
+
+ - name: Run tests (SQLite)
+ if: matrix.database == 'sqlite'
+ env:
+ DB_DRIVER: sqlite
+ DB_PATH: ":memory:"
+ run: make test
+
+ - name: Upload coverage
+ uses: codecov/codecov-action@v3
+ with:
+ file: ./coverage.out
+ flags: ${{ matrix.database }}
+```
+
+## Exit Criteria
+
+- [ ] All unit tests pass (both drivers): `make test`
+- [ ] All integration tests pass (both drivers): `make test-integration`
+- [ ] Code coverage β₯ 80% for new SQLite code
+- [ ] Performance benchmarks meet targets (documented above)
+- [ ] No regressions in PostgreSQL tests
+- [ ] CI/CD matrix tests pass (both drivers)
+- [ ] Golden files updated and validated
+- [ ] Metrics assertions pass
+- [ ] Linting passes: `make lint`
+- [ ] Race detector passes: `go test -race ./...`
+- [ ] Memory leaks checked with `pprof`
+- [ ] All test fixtures valid and loadable
+- [ ] Test helpers documented and reusable
+
+## Test Execution Commands
+
+```bash
+# Run all tests
+make test
+
+# Run database-specific tests
+go test ./engine/infra/sqlite/... -v
+
+# Run parameterized tests (both drivers)
+go test ./test/integration/database/... -v
+
+# Run with race detector
+go test -race ./engine/infra/sqlite/...
+
+# Run performance benchmarks
+go test -bench=. -benchmem ./test/integration/database/performance_test.go
+
+# Generate coverage report
+go test -coverprofile=coverage.out ./...
+go tool cover -html=coverage.out
+
+# Run specific test
+go test -v ./engine/infra/sqlite -run TestTaskRepo/Should_upsert_task_state
+```
+
+## Known Test Challenges
+
+### Challenge 1: SQLite Concurrency
+
+**Issue:** SQLite write serialization may cause tests to be flaky
+
+**Mitigation:**
+- Use `t.Parallel()` judiciously (not for write-heavy tests)
+- Add retries for "database locked" errors
+- Conservative concurrency in tests (5-10 workflows max)
+
+### Challenge 2: In-Memory vs File-Based
+
+**Issue:** Different behavior between `:memory:` and file-based SQLite
+
+**Mitigation:**
+- Test both modes explicitly
+- Use in-memory for unit tests (speed)
+- Use file-based for integration tests (realistic)
+
+### Challenge 3: Migration Testing
+
+**Issue:** Need to test migrations for both databases
+
+**Mitigation:**
+- Parameterized migration tests
+- Separate migration files per driver
+- Schema comparison tests
+
+---
+
+**Plan Version:** 1.0
+**Date:** 2025-01-27
+**Status:** Ready for Implementation
diff --git a/tasks/prd-temporal/README.md b/tasks/prd-temporal/README.md
new file mode 100644
index 00000000..b320486f
--- /dev/null
+++ b/tasks/prd-temporal/README.md
@@ -0,0 +1,86 @@
+# Temporal Standalone Mode - Task Breakdown
+
+Complete task breakdown for implementing embedded Temporal server in Compozy.
+
+## π Planning Documents
+
+- **`_techspec.md`** - Technical specification and implementation design
+- **`_docs.md`** - Documentation plan (7 pages)
+- **`_examples.md`** - Examples plan (7 projects)
+- **`_tests.md`** - Testing plan (unit + integration)
+- **`_tasks.md`** - Task summary with execution plan and dependencies
+
+## π― Task Files
+
+### Foundation (3 days)
+- **`_task_01.md`** - Embedded Server Package Foundation (L)
+ - Creates `engine/worker/embedded/` package
+ - Config types, validation, builders
+ - Blocks all other tasks
+
+### Core Development (2 days, parallel after Task 1)
+- **`_task_02.md`** - Embedded Server Lifecycle (M)
+ - Server Start/Stop implementation
+ - Ready-state polling
+
+- **`_task_03.md`** - Configuration System Extension (M)
+ - Mode selection and standalone config
+ - Registry entries and defaults
+
+### Integration (2 days)
+- **`_task_04.md`** - UI Server Implementation (M)
+ - Optional Web UI wrapper
+ - Can start after Task 2
+
+- **`_task_05.md`** - Server Lifecycle Integration (M)
+ - Dependencies.go integration
+ - Requires Tasks 2 & 3
+
+### Validation & Polish (3 days, parallel after Task 5)
+- **`_task_06.md`** - Core Integration Tests (L)
+ - Critical path validation
+
+- **`_task_07.md`** - CLI & Schema Updates (S)
+ - CLI flags and JSON schema
+
+### Documentation (2-3 days, parallel after Task 5)
+- **`_task_08.md`** - Documentation (L)
+ - 7 documentation pages
+
+- **`_task_09.md`** - Examples (L)
+ - 7 example projects
+
+### Advanced Testing (2 days, parallel after Task 5)
+- **`_task_10.md`** - Advanced Integration Tests (M)
+ - Error handling and edge cases
+
+## π Quick Start
+
+1. Read `_techspec.md` for implementation details
+2. Start with `_task_01.md` (foundation)
+3. Follow dependency order in `_tasks.md`
+4. Reference `_tests.md` for test requirements
+5. Use `_docs.md` and `_examples.md` for user-facing deliverables
+
+## π Execution Timeline
+
+**Critical Path:** 10 days (1.0 β 2.0||3.0 β 5.0 β 6.0)
+**Total Effort:** ~20 developer-days
+**Parallelization:** Can be completed in 10-11 days with 2-3 developers
+
+## π― Success Criteria
+
+- [ ] Workflows execute in standalone mode
+- [ ] Web UI accessible at http://localhost:8233
+- [ ] File-based persistence works
+- [ ] In-memory mode works
+- [ ] All tests pass (`make test`)
+- [ ] No linter errors (`make lint`)
+- [ ] Documentation complete
+- [ ] Examples tested and working
+
+## π Reference
+
+- Reference implementation: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+- Temporal server package: `go.temporal.io/server`
+- UI server package: `go.temporal.io/server/ui-server/v2`
diff --git a/tasks/prd-temporal/_docs.md b/tasks/prd-temporal/_docs.md
new file mode 100644
index 00000000..c65a8dea
--- /dev/null
+++ b/tasks/prd-temporal/_docs.md
@@ -0,0 +1,281 @@
+# Documentation Plan: Temporal Standalone Mode
+
+## Goals
+
+- Define all documentation updates required to ship Temporal standalone mode using `temporal.NewServer()`
+- Provide precise file paths, page outlines, and cross-link updates
+- Ensure users understand when to use standalone vs remote mode
+- Clarify that standalone uses production-grade Temporal server code
+
+## New/Updated Pages
+
+### 1. docs/content/docs/deployment/temporal-modes.mdx (NEW)
+- **Purpose:** Comprehensive guide to Temporal connection modes
+- **Outline:**
+ - Overview of Remote vs Standalone modes
+ - When to use each mode
+ - Remote Mode (Production)
+ - Requirements (external Temporal cluster)
+ - Configuration example
+ - High availability setup
+ - Standalone Mode (Development/Testing)
+ - What is it (embedded `temporal.NewServer()`)
+ - Why NOT Temporalite (deprecated)
+ - In-memory vs file-based persistence
+ - Configuration examples
+ - Web UI access (http://localhost:8233)
+ - Limitations and warnings
+ - Performance characteristics
+ - Architecture comparison (diagram: 4 services in standalone)
+ - Migration between modes
+ - Troubleshooting common issues (port conflicts, startup timeouts)
+- **Links:**
+ - Link to Configuration Reference
+ - Link to Quick Start guide
+ - Link to Temporal official docs
+ - Link to GitHub reference implementation
+
+### 2. docs/content/docs/configuration/temporal.mdx (UPDATE)
+- **Purpose:** Update Temporal configuration reference with new mode fields
+- **Updates:**
+ - Add `mode` field documentation
+ - Add `standalone` configuration section
+ - Add examples for both modes
+ - Add warning callouts about production use
+ - Document all 4 service ports (7233-7236)
+ - Document UI server (port 8233)
+- **New Sections:**
+ - "Mode Selection" (remote vs standalone)
+ - "Standalone Configuration" (database_file, frontend_port, bind_ip, enable_ui, ui_port, log_level)
+ - "Environment Variables" (TEMPORAL_MODE, TEMPORAL_STANDALONE_*)
+ - "Port Configuration" (frontend, history, matching, worker, UI)
+
+### 3. docs/content/docs/quick-start/index.mdx (UPDATE)
+- **Purpose:** Update quick start to use standalone mode by default for better onboarding
+- **Updates:**
+ - Change from "Run Docker Compose" to "Run with standalone mode"
+ - Add note about Docker still being an option
+ - Show standalone mode in example compozy.yaml
+ - Update "What's Next" section to mention production deployment
+ - Add note about Web UI at http://localhost:8233
+- **New Content:**
+ - Step: "Start Compozy with standalone Temporal"
+ - Callout: "For production, see Deployment β Temporal Modes"
+ - Tip: "Access Temporal Web UI at http://localhost:8233"
+
+### 4. docs/content/docs/deployment/production.mdx (UPDATE)
+- **Purpose:** Emphasize remote mode requirement for production
+- **Updates:**
+ - Add "Temporal Configuration" section
+ - Explicitly state standalone mode is NOT for production
+ - Explain why (single node, SQLite limitations, no HA)
+ - Link to temporal-modes.mdx for details
+ - Show production-ready remote mode configuration
+ - Recommend external Temporal cluster setup
+
+### 5. docs/content/docs/cli/compozy-start.mdx (UPDATE)
+- **Purpose:** Document `--temporal-mode` CLI flag
+- **Updates:**
+ - Add flag description
+ - Show usage examples
+ - Note precedence (CLI > env > config file)
+ - Document Web UI access when standalone mode enabled
+
+### 6. docs/content/docs/architecture/embedded-temporal.mdx (NEW)
+- **Purpose:** Technical deep-dive on embedded Temporal implementation
+- **Outline:**
+ - Architecture overview
+ - Four-service design (frontend, history, matching, worker)
+ - SQLite persistence layer
+ - Namespace management
+ - Web UI server integration
+ - Port allocation strategy
+ - Startup and shutdown lifecycle
+ - Comparison with external Temporal cluster
+ - Performance characteristics
+ - Security considerations (localhost-only by default)
+- **Audience:** Advanced users, contributors
+- **Links:**
+ - Link to Temporal server documentation
+ - Link to reference GitHub implementation
+
+## Schema Docs
+
+### 1. docs/content/docs/reference/schemas/config.mdx (UPDATE)
+- **Renders:** `schemas/config.json`
+- **Notes:**
+ - Highlight new `temporal.mode` enum
+ - Highlight new `temporal.standalone` object
+ - Add validation notes (mode must be "remote" or "standalone")
+ - Document all standalone fields with examples
+
+### 2. schemas/config.json (UPDATE via schemagen)
+- **Updates Required:**
+ - Add `mode` property to `temporal` object (enum: ["remote", "standalone"], default: "remote")
+ - Add `standalone` property to `temporal` object (object type)
+ - Define `standalone` schema with properties:
+ - `database_file` (string, default ":memory:")
+ - `frontend_port` (integer, min 0, max 65535, default 7233)
+ - `bind_ip` (string, default "127.0.0.1")
+ - `enable_ui` (boolean, default true)
+ - `ui_port` (integer, min 0, max 65535, default 8233)
+ - `log_level` (enum: ["debug", "info", "warn", "error"], default "warn")
+
+## API Docs
+
+No API changes required. Temporal mode is a server configuration concern only.
+
+## CLI Docs
+
+### 1. docs/content/docs/cli/global-flags.mdx (UPDATE)
+- **Add Flag:** `--temporal-mode`
+ - Description: "Temporal connection mode (remote or standalone)"
+ - Type: string
+ - Default: "remote"
+ - Env var: TEMPORAL_MODE
+ - Example: `compozy start --temporal-mode=standalone`
+ - Note: "Standalone mode starts embedded Temporal server"
+
+### 2. docs/content/docs/cli/compozy-config.mdx (UPDATE)
+- **Purpose:** Show standalone mode in `compozy config show` output
+- **Updates:**
+ - Add example output showing temporal.mode field
+ - Add example output showing temporal.standalone fields
+ - Show warning message when standalone mode active
+
+## Cross-page Updates
+
+### 1. docs/content/docs/architecture/overview.mdx (UPDATE)
+- **Update:** Infrastructure diagram showing optional embedded Temporal
+- **Note:** Add annotation "Temporal (remote or embedded via temporal.NewServer())"
+- **Add:** Brief mention of standalone mode in architecture section
+
+### 2. docs/content/index.mdx (Homepage) (UPDATE)
+- **Update:** Add bullet point under features: "Zero-dependency local development with embedded Temporal server"
+- **Update:** Quick start code snippet to show standalone mode
+- **Add:** "No Docker required for local development" callout
+
+### 3. docs/content/docs/installation/docker.mdx (UPDATE)
+- **Note:** Mention that Docker Compose for Temporal is optional with standalone mode
+- **Add:** Link to temporal-modes.mdx for comparison
+- **Keep:** Docker instructions for those who prefer external Temporal
+
+### 4. docs/content/docs/troubleshooting/temporal.mdx (NEW or UPDATE)
+- **Add Section:** "Standalone Mode Issues"
+- **Common Issues:**
+ - Port conflicts (7233-7236, 8233)
+ - Startup timeout
+ - SQLite file permissions
+ - Database corruption recovery
+- **Solutions:**
+ - How to configure alternative ports
+ - How to increase startup timeout
+ - How to fix permissions
+ - How to recover from corruption
+
+## Navigation & Indexing
+
+### Update docs/source.config.ts
+
+**Add new page to Deployment section:**
+```typescript
+{
+ title: "Temporal Modes",
+ url: "/docs/deployment/temporal-modes",
+ description: "Choose between remote and standalone Temporal modes"
+}
+```
+
+**Add new page to Architecture section:**
+```typescript
+{
+ title: "Embedded Temporal",
+ url: "/docs/architecture/embedded-temporal",
+ description: "Technical deep-dive on embedded Temporal server implementation"
+}
+```
+
+**Add new troubleshooting page:**
+```typescript
+{
+ title: "Temporal Troubleshooting",
+ url: "/docs/troubleshooting/temporal",
+ description: "Common Temporal issues and solutions"
+}
+```
+
+**Ensure order:**
+- Deployment section: Production β **Temporal Modes** (NEW) β Docker β Kubernetes
+- Architecture section: Overview β Components β **Embedded Temporal** (NEW) β Workflows β Tasks
+- Troubleshooting section: General β **Temporal** (NEW) β Database β Performance
+
+## Acceptance Criteria
+
+- [ ] All 7 new/updated content pages exist with correct outlines
+- [ ] Schema docs render the updated config.json with temporal mode fields
+- [ ] CLI docs show `--temporal-mode` flag with examples
+- [ ] Quick start uses standalone mode by default
+- [ ] Production docs explicitly warn against standalone mode
+- [ ] Embedded Temporal architecture doc explains four-service design
+- [ ] Troubleshooting guide covers common standalone mode issues
+- [ ] Cross-page links verified (no 404s)
+- [ ] Navigation sidebar shows all new pages in correct sections
+- [ ] Docs dev server builds without errors
+- [ ] Search index includes "standalone", "embedded temporal", "temporal.NewServer"
+- [ ] Code examples in docs are syntactically correct
+- [ ] All diagrams clearly show standalone vs remote architecture
+
+## Implementation Notes
+
+**Priority Order:**
+1. Schema updates (schemas/config.json) - Foundation for all docs
+2. Configuration reference (temporal.mdx) - Core documentation
+3. Temporal modes guide (temporal-modes.mdx) - Deep dive
+4. Embedded Temporal architecture (embedded-temporal.mdx) - Technical details
+5. Quick start update - Onboarding experience
+6. Production docs update - Safety warnings
+7. Troubleshooting guide - Support resource
+8. CLI docs - Reference material
+9. Cross-page updates - Consistency
+10. Navigation config - Discoverability
+
+**Content Guidelines:**
+- Use warning callouts for "Standalone mode is NOT for production"
+- Use info callouts to explain "Uses production-grade temporal.NewServer()"
+- Use code blocks with YAML syntax highlighting
+- Include "See also" sections linking related docs
+- Use tables for mode comparison (Remote vs Standalone)
+- Use diagrams to show four-service architecture
+- Keep explanations concise (prefer bullet points over paragraphs)
+- Mention Web UI prominently (differentiator from test-only solutions)
+
+**Visual Assets Needed:**
+- Diagram: Temporal architecture (remote: external cluster; standalone: 4 services embedded)
+- Diagram: Port allocation (7233-7236 for services, 8233 for UI)
+- Screenshot: `compozy config show` output with standalone mode
+- Screenshot: Temporal Web UI at localhost:8233
+- Optional: Animated GIF showing instant startup with standalone mode
+
+**Testing:**
+- Run `cd docs && npm run dev` to verify local build
+- Test all internal links
+- Verify schema rendering with updated config.json
+- Check mobile responsiveness of tables and diagrams
+- Verify code block syntax highlighting
+
+**Key Messages to Emphasize:**
+1. Standalone mode uses production-grade `temporal.NewServer()`, NOT deprecated Temporalite
+2. Web UI included by default for better debugging experience
+3. Four-service architecture mirrors production deployment
+4. Zero Docker dependency for local development
+5. NOT for production (single node, SQLite limitations)
+
+## Related Planning Artifacts
+- tasks/prd-temporal/_techspec.md
+- tasks/prd-temporal/_examples.md
+- tasks/prd-temporal/_tests.md
+
+## References
+- GitHub Reference: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+- Temporal Server Docs: https://docs.temporal.io/self-hosted-guide
+- Temporalite Deprecation: https://github.com/temporalio/temporalite#deprecation-notice
diff --git a/tasks/prd-temporal/_examples.md b/tasks/prd-temporal/_examples.md
new file mode 100644
index 00000000..fa54b1dc
--- /dev/null
+++ b/tasks/prd-temporal/_examples.md
@@ -0,0 +1,498 @@
+# Examples Plan: Temporal Standalone Mode
+
+## Conventions
+
+- Folder prefix: `examples/temporal-standalone/*`
+- Use standalone mode by default for all examples (better DX)
+- Demonstrate `temporal.NewServer()` approach (NOT Temporalite)
+- Use `:memory:` for ephemeral demos, file paths for persistence demos
+- Show Web UI access (http://localhost:8233)
+- No secrets or external dependencies
+
+## Example Matrix
+
+### 1. examples/temporal-standalone/basic
+**Purpose:** Demonstrate simplest possible setup with standalone mode
+**Files:**
+- `compozy.yaml` β Minimal config with `temporal.mode: standalone`
+- `workflows/hello.yaml` β Basic workflow (single task, print message)
+- `README.md` β Quick start guide
+- `.gitignore` β Exclude temporal.db if generated
+**Demonstrates:**
+- Zero-config startup (no Docker required)
+- Instant workflow execution (<5 seconds from start to first workflow)
+- In-memory persistence (no database file)
+- Four-service architecture (frontend, history, matching, worker)
+- Web UI access for debugging
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/basic
+compozy start
+# Server starts with embedded Temporal (4 services + UI)
+# Open Web UI: http://localhost:8233
+# In another terminal:
+compozy workflow trigger hello --input='{"name": "World"}'
+# View workflow in Web UI
+```
+**Expected Output:**
+- Server starts in <5 seconds
+- Logs show: "Starting embedded Temporal server (mode=standalone, database=:memory:, ui_enabled=true)"
+- Logs show: "Embedded Temporal server started successfully (frontend_addr=127.0.0.1:7233, ui_addr=http://127.0.0.1:8233)"
+- Workflow completes successfully
+- Web UI shows workflow execution
+
+### 2. examples/temporal-standalone/persistent
+**Purpose:** Show file-based persistence across server restarts
+**Files:**
+- `compozy.yaml` β Standalone mode with `database_file: ./data/temporal.db`
+- `workflows/counter.yaml` β Workflow that increments a counter
+- `data/` β Directory for SQLite database (created on first run)
+- `README.md` β Guide to persistence behavior
+- `.gitignore` β Exclude `data/temporal.db` from git
+**Demonstrates:**
+- SQLite file-based persistence
+- Workflow state survives server restart
+- Database file management
+- WAL mode for better reliability
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/persistent
+mkdir -p data
+compozy start &
+sleep 2
+# Trigger workflow
+compozy workflow trigger counter
+ls -lh data/temporal.db # Database file created (~50KB)
+# Stop server
+killall compozy
+# Restart - workflow state persists
+compozy start &
+sleep 2
+compozy workflow describe counter # Shows workflow history from before restart
+```
+**Expected Output:**
+- Database file created in `data/temporal.db`
+- File grows as workflows execute
+- Workflow history persists across restarts
+- WAL mode files (`.db-shm`, `.db-wal`) present
+
+### 3. examples/temporal-standalone/custom-ports
+**Purpose:** Demonstrate custom port configuration
+**Files:**
+- `compozy.yaml` β Standalone with custom ports (8233 frontend, 9233 UI)
+- `workflows/demo.yaml` β Simple workflow
+- `README.md` β Port configuration guide
+**Demonstrates:**
+- Configurable ports for all services
+- Avoiding port conflicts
+- Custom UI port
+- Multiple standalone instances on same machine
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/custom-ports
+compozy start
+# Frontend on 8233 instead of 7233
+# Web UI on 9233 instead of 8233
+# Open Web UI: http://localhost:9233
+```
+**Config:**
+```yaml
+temporal:
+ mode: standalone
+ standalone:
+ frontend_port: 8233
+ ui_port: 9233
+```
+
+### 4. examples/temporal-standalone/no-ui
+**Purpose:** Disable Web UI for minimal resource usage
+**Files:**
+- `compozy.yaml` β Standalone with `enable_ui: false`
+- `workflows/minimal.yaml` β Lightweight workflow
+- `README.md` β Minimal configuration guide
+**Demonstrates:**
+- Disabling Web UI
+- Faster startup
+- Lower resource usage
+- Headless operation for CI
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/no-ui
+compozy start
+# No Web UI server started
+# Faster startup, lower memory usage
+```
+
+### 5. examples/temporal-standalone/debugging
+**Purpose:** Advanced debugging with Web UI and debug logging
+**Files:**
+- `compozy.yaml` β Standalone with `log_level: debug`
+- `workflows/buggy.yaml` β Workflow with intentional error
+- `workflows/fixed.yaml` β Corrected version
+- `README.md` β Debugging techniques guide
+**Demonstrates:**
+- Debug logging from Temporal server
+- Using Web UI for workflow inspection
+- Examining workflow history
+- Error handling and retry behavior
+- Viewing activity timeouts
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/debugging
+compozy start
+# Debug logs show Temporal internals
+# Trigger buggy workflow
+compozy workflow trigger buggy
+# Open Web UI: http://localhost:8233
+# Inspect error in Web UI (shows stack trace, retry attempts)
+# Fix and retry
+compozy workflow trigger fixed
+```
+
+### 6. examples/temporal-standalone/migration-from-remote
+**Purpose:** Switching between remote and standalone modes
+**Files:**
+- `compozy.remote.yaml` β Remote mode configuration
+- `compozy.standalone.yaml` β Equivalent standalone configuration
+- `workflows/demo.yaml` β Same workflow works in both modes
+- `docker-compose.yml` β Optional external Temporal for remote mode
+- `README.md` β Migration guide
+**Demonstrates:**
+- Configuration differences between modes
+- Workflow compatibility (no code changes)
+- When to use each mode
+- Migration procedure
+- Zero code changes required
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/migration-from-remote
+
+# Option 1: Remote mode (requires Docker)
+docker-compose up -d
+compozy start --config=compozy.remote.yaml
+
+# Option 2: Standalone mode (no Docker)
+compozy start --config=compozy.standalone.yaml
+
+# Same workflows work in both modes (zero code changes)
+```
+
+### 7. examples/temporal-standalone/integration-testing
+**Purpose:** Integration testing with standalone Temporal
+**Files:**
+- `compozy.yaml` β Standalone with ephemeral ports for parallel tests
+- `workflows/calculator.yaml` β Deterministic workflow for testing
+- `tests/integration_test.go` β Go integration tests using standalone mode
+- `Makefile` β Test runner
+- `README.md` β Testing guide
+**Demonstrates:**
+- Using standalone mode in automated tests
+- Ephemeral ports (no port conflicts)
+- Fast test execution (no Docker startup)
+- Parallel test execution
+- Deterministic workflow testing
+**Walkthrough:**
+```bash
+cd examples/temporal-standalone/integration-testing
+make test
+# Runs integration tests with isolated standalone Temporal instances
+# Each test gets its own Temporal server (ephemeral ports)
+# Tests complete in <30 seconds
+```
+
+## Minimal YAML Shapes
+
+### Basic Standalone Configuration
+```yaml
+# compozy.yaml - Simplest standalone mode
+temporal:
+ mode: standalone
+ namespace: default
+ task_queue: my-app-queue
+ standalone:
+ database_file: ":memory:"
+ # Defaults: frontend_port=7233, bind_ip=127.0.0.1, enable_ui=true, ui_port=8233
+```
+
+### Persistent Standalone Configuration
+```yaml
+# compozy.yaml - File-based persistence
+temporal:
+ mode: standalone
+ namespace: default
+ standalone:
+ database_file: ./data/temporal.db
+ frontend_port: 7233
+ enable_ui: true
+ ui_port: 8233
+ log_level: info
+```
+
+### Custom Ports Configuration
+```yaml
+# compozy.yaml - Custom ports to avoid conflicts
+temporal:
+ mode: standalone
+ standalone:
+ database_file: ":memory:"
+ frontend_port: 8233 # Frontend on 8233 instead of 7233
+ ui_port: 9233 # UI on 9233 instead of 8233
+```
+
+### No UI Configuration
+```yaml
+# compozy.yaml - Minimal resource usage, no Web UI
+temporal:
+ mode: standalone
+ standalone:
+ database_file: ":memory:"
+ enable_ui: false # Disable Web UI
+ log_level: warn # Less verbose logging
+```
+
+### Debug Configuration
+```yaml
+# compozy.yaml - Maximum visibility for debugging
+temporal:
+ mode: standalone
+ standalone:
+ database_file: ":memory:"
+ enable_ui: true
+ log_level: debug # Verbose Temporal server logs
+```
+
+### Remote Mode Configuration (for comparison)
+```yaml
+# compozy.yaml - External Temporal cluster
+temporal:
+ mode: remote # Default, can be omitted
+ host_port: localhost:7233
+ namespace: default
+ task_queue: my-app-queue
+```
+
+### Environment Variable Override
+```bash
+# Use standalone mode via env var (overrides config file)
+export TEMPORAL_MODE=standalone
+export TEMPORAL_STANDALONE_DATABASE_FILE=":memory:"
+export TEMPORAL_STANDALONE_ENABLE_UI=true
+compozy start
+```
+
+## Test & CI Coverage
+
+### Integration Tests to Add
+
+**test/integration/temporal/standalone_test.go:**
+- TestStandaloneMemoryMode - In-memory workflow execution
+- TestStandaloneFileMode - Persistent SQLite execution
+- TestStandaloneServerLifecycle - Start/stop behavior
+- TestStandaloneWorkflowExecution - End-to-end workflow
+- TestStandaloneMultipleNamespaces - Namespace isolation
+- TestStandaloneWebUI - UI server accessible when enabled
+- TestStandaloneNoUI - UI server not started when disabled
+- TestStandaloneCustomPorts - Custom port configuration
+
+**test/integration/temporal/mode_switching_test.go:**
+- TestDefaultModeIsRemote - Validates default behavior
+- TestStandaloneModeActivation - Config-based mode switch
+- TestStandaloneStartupFailure - Error handling
+- TestStandalonePortConflict - Port collision handling
+- TestStandaloneTimeout - Startup timeout behavior
+
+**test/integration/temporal/persistence_test.go:**
+- TestStandalonePersistence - Workflow state survives restart
+- TestStandaloneWALMode - WAL mode enabled for file-based
+- TestStandaloneDatabaseCleanup - Cleanup on server stop
+
+## Runbooks per Example
+
+### basic/
+**Prereqs:** None (zero dependencies!)
+**Commands:**
+```bash
+cd examples/temporal-standalone/basic
+compozy start & # Start server in background
+sleep 5 # Wait for startup
+# Server logs show:
+# - "Starting embedded Temporal server"
+# - "Embedded Temporal server started successfully"
+# - "Temporal Web UI: http://127.0.0.1:8233"
+open http://localhost:8233 # Open Web UI
+compozy workflow trigger hello --input='{"name": "Developer"}'
+compozy workflow list # See workflow history
+killall compozy # Stop server
+```
+**Expected Output:**
+- Server starts in <5 seconds
+- Web UI accessible immediately
+- Workflow completes successfully
+- Output: "Hello, Developer"
+- Web UI shows workflow in Completed state
+
+### persistent/
+**Prereqs:** Write permissions in `./data/`
+**Commands:**
+```bash
+cd examples/temporal-standalone/persistent
+mkdir -p data
+compozy start &
+sleep 5
+compozy workflow trigger counter
+ls -lh data/temporal.db* # Database file + WAL files
+# Stop server
+killall compozy
+# Restart - workflow state persists
+compozy start &
+sleep 5
+compozy workflow describe counter # Shows workflow history
+```
+**Expected Output:**
+- Database file created: `data/temporal.db` (~50KB)
+- WAL files: `data/temporal.db-shm`, `data/temporal.db-wal`
+- Workflow history survives restart
+- Counter state persists
+
+### custom-ports/
+**Prereqs:** Ports 8233-8236 and 9233 available
+**Commands:**
+```bash
+cd examples/temporal-standalone/custom-ports
+compozy start &
+sleep 5
+# Frontend on 8233, UI on 9233
+open http://localhost:9233 # Custom UI port
+compozy workflow trigger demo
+```
+**Expected Output:**
+- Server starts on custom ports
+- Logs show: "frontend_addr=127.0.0.1:8233, ui_addr=http://127.0.0.1:9233"
+- Web UI accessible on port 9233
+
+### no-ui/
+**Prereqs:** None
+**Commands:**
+```bash
+cd examples/temporal-standalone/no-ui
+compozy start &
+sleep 3 # Faster startup without UI
+# No Web UI server started
+compozy workflow trigger minimal
+```
+**Expected Output:**
+- Faster startup (<3 seconds)
+- Logs show: "ui_enabled=false"
+- Lower memory usage (~50MB less without UI)
+
+### debugging/
+**Prereqs:** None
+**Commands:**
+```bash
+cd examples/temporal-standalone/debugging
+compozy start &
+sleep 5
+# Debug logs show Temporal internals
+compozy workflow trigger buggy --input='{"value": -1}'
+# Workflow fails with validation error
+open http://localhost:8233 # Inspect in Web UI
+# Fix and retry
+compozy workflow trigger fixed --input='{"value": 42"}'
+```
+**Expected Output:**
+- Debug logs show workflow execution details
+- Buggy workflow fails with clear error
+- Web UI shows error details and retry attempts
+- Fixed workflow completes successfully
+
+### migration-from-remote/
+**Prereqs:** Docker (for remote mode option only)
+**Commands:**
+```bash
+cd examples/temporal-standalone/migration-from-remote
+# Test standalone mode
+compozy start --config=compozy.standalone.yaml &
+sleep 5
+compozy workflow trigger demo
+killall compozy
+
+# Test remote mode (requires Docker)
+docker-compose up -d
+sleep 10 # Wait for Temporal to start
+compozy start --config=compozy.remote.yaml &
+sleep 2
+compozy workflow trigger demo
+docker-compose down
+```
+**Expected Output:**
+- Same workflow runs in both modes
+- No code changes required
+- Output identical regardless of mode
+
+### integration-testing/
+**Prereqs:** Go 1.23+, make
+**Commands:**
+```bash
+cd examples/temporal-standalone/integration-testing
+make test
+# Or manually:
+go test -v ./tests/... -race -parallel=4
+```
+**Expected Output:**
+- All tests pass in <30 seconds
+- No port conflicts (ephemeral ports used)
+- Test coverage report generated
+- Logs show each test gets isolated Temporal instance
+
+## Acceptance Criteria
+
+- [ ] All 7 example directories exist with complete files
+- [ ] Each example has a comprehensive README with:
+ - Purpose and key concepts
+ - Prerequisites
+ - Step-by-step walkthrough
+ - Expected output
+ - Troubleshooting section
+ - Link to reference implementation
+- [ ] All YAML files are valid and tested
+- [ ] All examples demonstrate Web UI access (except no-ui example)
+- [ ] Integration tests exist in `test/integration/temporal/`
+- [ ] Tests pass in CI pipeline
+- [ ] Examples are referenced in main documentation
+- [ ] Code in examples follows go-coding-standards.mdc
+- [ ] No secrets or credentials in example files
+- [ ] .gitignore properly excludes database files and binaries
+- [ ] Each example completes in <10 seconds
+
+## Implementation Priority
+
+1. **basic/** - Quickest win, best onboarding experience, shows Web UI
+2. **persistent/** - Shows advanced use case with file-based persistence
+3. **integration-testing/** - Critical for CI/CD adoption
+4. **debugging/** - Showcases Web UI capabilities
+5. **custom-ports/** - Solves common port conflict issues
+6. **no-ui/** - Minimal resource usage option
+7. **migration-from-remote/** - Eases transition
+
+## Notes
+
+- All examples should start in <5 seconds to highlight standalone mode's speed advantage
+- Emphasize Web UI access in READMEs (major differentiator from test-only solutions)
+- Use `compozy config diagnostics` in READMEs to show configuration sources
+- Include "What's Next" section in each README linking to relevant docs
+- Consider adding asciinema recordings for visual walkthroughs
+- Link examples to specific docs sections for deeper learning
+- Reference GitHub implementation: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+
+## Key Messages
+
+- **NOT Temporalite** - Uses production-grade `temporal.NewServer()`
+- **Web UI included** - Better debugging experience (http://localhost:8233)
+- **Four services** - Frontend, history, matching, worker (mirrors production)
+- **Zero Docker** - No external dependencies for local development
+- **Fast startup** - <5 seconds from start to ready
+
+## Related Planning Artifacts
+- tasks/prd-temporal/_techspec.md
+- tasks/prd-temporal/_docs.md
+- tasks/prd-temporal/_tests.md
diff --git a/tasks/prd-temporal/_task_01.md b/tasks/prd-temporal/_task_01.md
new file mode 100644
index 00000000..ab584741
--- /dev/null
+++ b/tasks/prd-temporal/_task_01.md
@@ -0,0 +1,85 @@
+# Task 1.0: Embedded Server Package Foundation
+
+## status: completed
+
+**Size:** L (3 days)
+**Priority:** CRITICAL - Blocks all other tasks
+**Dependencies:** None
+
+## Overview
+
+Create the `engine/worker/embedded/` package with core types, validation, builder functions, and namespace creation. This is the foundation for the embedded Temporal server.
+
+## Deliverables
+
+- [x] `engine/worker/embedded/config.go` - Config type and validation
+- [x] `engine/worker/embedded/builder.go` - Temporal config builders
+- [x] `engine/worker/embedded/namespace.go` - Namespace creation helper
+- [x] `engine/worker/embedded/config_test.go` - Config validation tests
+- [x] `engine/worker/embedded/builder_test.go` - Builder function tests
+- [x] `engine/worker/embedded/namespace_test.go` - Namespace tests
+- [x] `go.mod` - Add `go.temporal.io/server` dependency
+
+## Acceptance Criteria
+
+- [x] Package compiles successfully
+- [x] All unit tests pass
+- [x] Config validation catches invalid ports, bad database paths, invalid log levels
+- [x] Defaults applied correctly (FrontendPort=7233, BindIP="127.0.0.1", etc.)
+- [x] SQLite connect attributes built correctly for memory and file modes
+- [x] Static hosts configuration returns correct 4-service addresses
+- [x] Namespace creation logic implemented (will be tested in task 2.0)
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_techspec.md` sections:
+- "Core Interfaces" (lines 71-117) for Config struct
+- "Implementation Design" for builder patterns
+- "SQLite Configuration" for connection attributes
+
+**Key Functions:**
+- `validateConfig(*Config) error` - Validate all fields
+- `applyDefaults(*Config)` - Apply default values
+- `buildTemporalConfig(*Config) (*config.Config, error)` - Build Temporal server config
+- `buildSQLiteConnectAttrs(*Config) map[string]string` - SQLite connection params
+- `buildStaticHosts(*Config) map[string][]string` - Service host mapping
+- `createNamespace(*config.Config, *Config) error` - Namespace initialization
+
+## Tests (from _tests.md)
+
+**config_test.go:**
+- Should validate required fields
+- Should apply defaults correctly
+- Should build SQLite connect attributes
+- Should build static hosts configuration
+
+**builder_test.go:**
+- Should build valid Temporal config
+- Should configure SQLite persistence
+- Should configure services correctly
+
+**namespace_test.go:**
+- Should create namespace in SQLite
+- Should handle existing namespace gracefully
+
+## Files to Modify
+
+- `go.mod` - Add dependency: `go.temporal.io/server v1.24.2` (or latest)
+- `go.sum` - Auto-updated by go mod
+
+## Notes
+
+- Use context-first: `logger.FromContext(ctx)` for all logging
+- Keep functions under 50 lines
+- Reference implementation: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+
+## Validation
+
+```bash
+# Run scoped tests
+gotestsum --format pkgname -- -race -parallel=4 ./engine/worker/embedded
+
+# Run scoped lint
+golangci-lint run --fix --allow-parallel-runners ./engine/worker/embedded/...
+```
diff --git a/tasks/prd-temporal/_task_02.md b/tasks/prd-temporal/_task_02.md
new file mode 100644
index 00000000..17a9b8be
--- /dev/null
+++ b/tasks/prd-temporal/_task_02.md
@@ -0,0 +1,70 @@
+# Task 2.0: Embedded Server Lifecycle
+
+## status: completed
+
+**Size:** M (2 days)
+**Priority:** HIGH - Required for integration
+**Dependencies:** Task 1.0
+
+## Overview
+
+Implement the Server struct with lifecycle management (Start, Stop, ready-state polling). This enables starting and stopping the embedded Temporal server.
+
+## Deliverables
+
+- [x] `engine/worker/embedded/server.go` - Server lifecycle implementation
+- [x] `engine/worker/embedded/server_test.go` - Lifecycle tests
+
+## Acceptance Criteria
+
+- [x] `NewServer(ctx, cfg)` creates server without starting it
+- [x] `Start(ctx)` starts all 4 services (frontend, history, matching, worker)
+- [x] `waitForReady(ctx)` polls until frontend accepts connections
+- [x] `Stop(ctx)` gracefully shuts down all services
+- [x] `FrontendAddress()` returns correct address
+- [x] Timeout handling works (returns error if startup exceeds StartTimeout)
+- [x] Port conflicts detected with clear error messages
+- [x] All unit tests pass
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_techspec.md` "Core Interfaces" section (lines 119-205) for Server struct and methods.
+
+**Key Methods:**
+- `NewServer(context.Context, *Config) (*Server, error)` - Create server (no start)
+- `Start(context.Context) error` - Start server and wait for ready
+- `Stop(context.Context) error` - Graceful shutdown
+- `waitForReady(context.Context) error` - Poll frontend until accessible
+- `FrontendAddress() string` - Return frontend address
+
+## Tests (from _tests.md)
+
+**server_test.go:**
+- Should create server with valid config
+- Should reject invalid config
+- Should start server successfully
+- Should timeout if server doesn't start
+- Should stop server gracefully
+- Should handle port conflicts
+- Should wait for ready state
+
+## Files to Create
+
+- `engine/worker/embedded/server.go`
+- `engine/worker/embedded/server_test.go`
+
+## Notes
+
+- Use `temporal.NewServer()` with `temporal.WithConfig()`, `temporal.ForServices()`, etc.
+- Server creates 4 services on sequential ports (7233-7236 by default)
+- Ready-state polling: dial frontend gRPC port with timeout
+- Context propagation: pass ctx through all operations
+- UI server integration added in task 4.0
+
+## Validation
+
+```bash
+gotestsum --format pkgname -- -race -parallel=4 ./engine/worker/embedded
+golangci-lint run --fix --allow-parallel-runners ./engine/worker/embedded/...
+```
diff --git a/tasks/prd-temporal/_task_03.md b/tasks/prd-temporal/_task_03.md
new file mode 100644
index 00000000..79c96262
--- /dev/null
+++ b/tasks/prd-temporal/_task_03.md
@@ -0,0 +1,95 @@
+# Task 3.0: Configuration System Extension
+
+## status: completed
+
+**Size:** M (2 days)
+**Priority:** HIGH - Required for integration
+**Dependencies:** Task 1.0 (needs embedded.Config type)
+
+## Overview
+
+Extend `pkg/config` to support `TemporalConfig.Mode` and `StandaloneConfig`, add registry entries, defaults, and validation.
+
+## Deliverables
+
+- [x] `pkg/config/config.go` - Add Mode and StandaloneConfig fields
+- [x] `pkg/config/definition/schema.go` - Registry entries
+- [x] `pkg/config/provider.go` - Default values
+- [x] `pkg/config/config_test.go` - Config validation tests
+
+## Acceptance Criteria
+
+- [x] `TemporalConfig.Mode` field added (values: "remote", "standalone")
+- [x] `TemporalConfig.Standalone` field added (type: StandaloneConfig)
+- [x] StandaloneConfig matches embedded.Config structure
+- [x] Registry entries defined for all new fields
+- [x] Defaults applied: Mode="remote", Standalone.FrontendPort=7233, etc.
+- [x] Validation ensures valid mode values
+- [x] Validation ensures valid standalone config when mode="standalone"
+- [x] All tests pass
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_techspec.md` "Configuration Extension" section for field structure.
+
+**Changes to pkg/config/config.go:**
+```go
+type TemporalConfig struct {
+ HostPort string
+ Namespace string
+ TaskQueue string
+ Mode string // NEW: "remote" or "standalone"
+ Standalone StandaloneConfig // NEW: standalone settings
+}
+
+type StandaloneConfig struct {
+ DatabaseFile string
+ FrontendPort int
+ BindIP string
+ Namespace string
+ EnableUI bool
+ UIPort int
+ LogLevel string
+}
+```
+
+**Registry Entries (definition/schema.go):**
+- `temporal.mode` β Mode
+- `temporal.standalone.*` β All StandaloneConfig fields
+
+**Defaults (provider.go):**
+- Mode: "remote"
+- Standalone.FrontendPort: 7233
+- Standalone.BindIP: "127.0.0.1"
+- Standalone.EnableUI: true
+- Standalone.UIPort: 8233
+- Standalone.LogLevel: "warn"
+
+## Tests (from _tests.md)
+
+**config_test.go:**
+- Should validate Mode field (only "remote" or "standalone")
+- Should apply standalone defaults when Mode="standalone"
+- Should validate standalone config fields
+- Should allow HostPort override in standalone mode
+
+## Files to Modify
+
+- `pkg/config/config.go`
+- `pkg/config/definition/schema.go`
+- `pkg/config/provider.go`
+- `pkg/config/config_test.go`
+
+## Notes
+
+- Keep TemporalConfig.HostPort - it gets overridden at runtime in standalone mode
+- StandaloneConfig.Namespace defaults to TemporalConfig.Namespace
+- Use validation tags where applicable
+
+## Validation
+
+```bash
+gotestsum --format pkgname -- -race -parallel=4 ./pkg/config
+golangci-lint run --fix --allow-parallel-runners ./pkg/config/...
+```
diff --git a/tasks/prd-temporal/_task_04.md b/tasks/prd-temporal/_task_04.md
new file mode 100644
index 00000000..5fbf6916
--- /dev/null
+++ b/tasks/prd-temporal/_task_04.md
@@ -0,0 +1,76 @@
+# Task 4.0: UI Server Implementation
+
+**Size:** M (1-2 days)
+**Priority:** MEDIUM - Optional feature
+**Dependencies:** Tasks 1.0, 2.0
+
+## Overview
+
+Implement optional Temporal Web UI server wrapper for local development debugging.
+
+## status: completed
+
+## Deliverables
+
+- [x] `engine/worker/embedded/ui.go` - UI server implementation
+- [x] `engine/worker/embedded/ui_test.go` - UI server tests
+
+## Acceptance Criteria
+
+- [x] `UIServer` struct created
+- [x] `newUIServer(cfg *Config) *UIServer` constructor
+- [x] `Start(ctx)` starts UI server on configured port
+- [x] `Stop(ctx)` gracefully stops UI server
+- [x] UI connects to embedded Temporal frontend
+- [x] UI accessible at http://localhost:8233 (default)
+- [x] UI can be disabled via config (EnableUI=false)
+- [x] All tests pass
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_techspec.md` "UI Server Manager" section (lines 46-50).
+
+**Key Components:**
+- Use `go.temporal.io/server/ui-server/v2` package
+- UIServer wraps ui-server with lifecycle management
+- Connects to embedded frontend via HostPort
+- Supports graceful shutdown
+
+**Integration into Server struct:**
+- Server.uiServer field (*UIServer, nil if disabled)
+- Created in NewServer if cfg.EnableUI == true
+- Started in Server.Start() after temporal server ready
+- Stopped in Server.Stop() before temporal server
+
+## Tests (from _tests.md)
+
+**ui_test.go:**
+- Should create UI server with valid config
+- Should start UI server successfully
+- Should stop UI server gracefully
+- Should skip UI when disabled (EnableUI=false)
+- Should return error if UI port unavailable
+
+## Files to Create
+
+- `engine/worker/embedded/ui.go`
+- `engine/worker/embedded/ui_test.go`
+
+## Files to Modify
+
+- `engine/worker/embedded/server.go` - Integrate UI server into lifecycle
+- `go.mod` - Add `go.temporal.io/server/ui-server/v2` dependency
+
+## Notes
+
+- UI server is optional - startup should succeed even if UI fails (log warning)
+- UI port conflicts should be non-fatal (log error, continue without UI)
+- Access UI at: http://localhost:
+
+## Validation
+
+```bash
+gotestsum --format pkgname -- -race -parallel=4 ./engine/worker/embedded
+golangci-lint run --fix --allow-parallel-runners ./engine/worker/embedded/...
+```
diff --git a/tasks/prd-temporal/_task_05.md b/tasks/prd-temporal/_task_05.md
new file mode 100644
index 00000000..a87f3353
--- /dev/null
+++ b/tasks/prd-temporal/_task_05.md
@@ -0,0 +1,89 @@
+# Task 5.0: Server Lifecycle Integration
+
+## status: completed
+
+**Size:** M (2 days)
+**Priority:** HIGH - Enables end-to-end functionality
+**Dependencies:** Tasks 2.0, 3.0
+
+## Overview
+
+Integrate embedded Temporal server into main server startup sequence in `engine/infra/server/dependencies.go`.
+
+## Deliverables
+
+- [x] `engine/infra/server/dependencies.go` - Add maybeStartStandaloneTemporal function
+- [x] Integration point tested
+
+## Acceptance Criteria
+
+- [x] `maybeStartStandaloneTemporal(ctx, cfg)` function created
+- [x] Function called BEFORE `maybeStartWorker()` in setupDependencies
+- [x] Embedded server started when Mode="standalone"
+- [x] Nothing happens when Mode="remote"
+- [x] cfg.Temporal.HostPort dynamically overridden in standalone mode
+- [x] Cleanup function registered for graceful shutdown
+- [x] Startup logging added (Info: "Starting in standalone mode", Warn: "Not for production")
+- [x] Integration verified with manual test
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_techspec.md` "Server Lifecycle Integration" section.
+
+**Function Signature:**
+```go
+func maybeStartStandaloneTemporal(ctx context.Context, cfg *config.Config) (cleanup func(), err error)
+```
+
+**Logic:**
+1. Check if `cfg.Temporal.Mode == "standalone"`
+2. If false, return nil cleanup and nil error
+3. If true:
+ - Build embedded.Config from cfg.Temporal.Standalone
+ - Call embedded.NewServer(ctx, embeddedCfg)
+ - Call server.Start(ctx)
+ - Override cfg.Temporal.HostPort = server.FrontendAddress()
+ - Log startup info and production warning
+ - Return cleanup function that calls server.Stop()
+
+**Integration Point:**
+Insert in `setupDependencies()` between Temporal client creation prep and worker startup.
+
+## Tests
+
+Manual integration test:
+1. Set Mode="standalone" in config
+2. Start compozy server
+3. Verify embedded server starts
+4. Verify worker connects
+5. Execute simple workflow
+6. Verify UI accessible at http://localhost:8233
+7. Verify graceful shutdown
+
+## Files to Modify
+
+- `engine/infra/server/dependencies.go` - Add maybeStartStandaloneTemporal function and call it
+
+## Notes
+
+- Use `logger.FromContext(ctx)` for all logging
+- Override HostPort AFTER server starts (to get actual frontend address)
+- Register cleanup in cleanup chain (append to existing cleanups)
+- Log at Info level: "Temporal standalone mode started at "
+- Log at Warn level: "Temporal standalone mode is not recommended for production"
+
+## Validation
+
+```bash
+# Manual test
+compozy start --temporal-mode=standalone
+
+# Check logs for:
+# - "Temporal standalone mode started"
+# - Warning about production usage
+# - Worker connection success
+# - Workflow execution
+
+# Visit http://localhost:8233 to verify UI
+```
diff --git a/tasks/prd-temporal/_task_06.md b/tasks/prd-temporal/_task_06.md
new file mode 100644
index 00000000..47787d93
--- /dev/null
+++ b/tasks/prd-temporal/_task_06.md
@@ -0,0 +1,76 @@
+# Task 6.0: Core Integration Tests
+
+**Size:** L (3 days)
+**Priority:** CRITICAL - Validates core functionality
+**Dependencies:** Task 5.0
+
+## Overview
+
+Create comprehensive integration tests for standalone mode covering memory/file persistence, mode switching, and workflow execution.
+
+## Deliverables
+
+- [ ] `test/integration/temporal/standalone_test.go`
+- [ ] `test/integration/temporal/mode_switching_test.go`
+- [ ] `test/integration/temporal/persistence_test.go`
+- [ ] Test fixtures and testdata
+
+## Acceptance Criteria
+
+- [ ] In-memory mode test passes (ephemeral storage)
+- [ ] File-based mode test passes (persistent storage)
+- [ ] Custom ports test passes
+- [ ] Workflow execution test passes (end-to-end)
+- [ ] Mode switching test passes (default is remote)
+- [ ] Persistence test passes (restart with same database)
+- [ ] All tests use `t.Context()`, not `context.Background()`
+- [ ] All tests pass
+- [ ] No linter errors
+
+## Implementation Approach
+
+See `_tests.md` "Integration Tests" section for detailed test cases.
+
+**standalone_test.go:**
+- `TestStandaloneMemoryMode` - DatabaseFile=":memory:", verify ephemeral
+- `TestStandaloneFileMode` - DatabaseFile="./test.db", verify persistent
+- `TestStandaloneCustomPorts` - FrontendPort=17233, verify services on custom ports
+- `TestStandaloneWorkflowExecution` - Execute simple workflow end-to-end
+
+**mode_switching_test.go:**
+- `TestDefaultModeIsRemote` - No config β remote mode
+- `TestStandaloneModeActivation` - Mode="standalone" β embedded starts
+
+**persistence_test.go:**
+- `TestStandalonePersistence` - Start server, create workflow, stop, restart, verify workflow still exists
+
+## Test Patterns
+
+Use test helpers from `test/helpers/`:
+- `SetupWorkflowEnvironment()` for common setup
+- Cleanup with `t.Cleanup()`
+- Use temporary directories for database files
+
+## Files to Create
+
+- `test/integration/temporal/standalone_test.go`
+- `test/integration/temporal/mode_switching_test.go`
+- `test/integration/temporal/persistence_test.go`
+
+## Notes
+
+- Tests MUST use real embedded Temporal server (no mocks)
+- Use `t.TempDir()` for database files
+- Clean up server with `defer server.Stop(ctx)`
+- Allow generous timeouts for CI (30s+)
+- Skip slow tests with `testing.Short()`
+
+## Validation
+
+```bash
+# Run integration tests
+gotestsum --format pkgname -- -race -parallel=4 ./test/integration/temporal
+
+# Run full test suite
+make test
+```
diff --git a/tasks/prd-temporal/_task_07.md b/tasks/prd-temporal/_task_07.md
new file mode 100644
index 00000000..19158d50
--- /dev/null
+++ b/tasks/prd-temporal/_task_07.md
@@ -0,0 +1,80 @@
+# Task 7.0: CLI & Schema Updates
+
+## status: completed
+
+**Size:** S (half day)
+**Priority:** MEDIUM - CLI support
+**Dependencies:** Task 3.0
+
+## Overview
+
+Add CLI flags for temporal mode configuration and update JSON schema.
+
+## Deliverables
+
+- [x] CLI flag: `--temporal-mode`
+- [x] CLI flag: `--temporal-standalone-database`
+- [x] CLI flag: `--temporal-standalone-frontend-port`
+- [x] CLI flag: `--temporal-standalone-ui-port`
+- [x] `schemas/config.json` - Add new fields
+- [x] Documentation: `cli/help/global-flags.md`
+
+## Acceptance Criteria
+
+- [x] `--temporal-mode` flag accepts "remote" or "standalone"
+- [x] Standalone flags only relevant when mode="standalone"
+- [x] Flags override YAML config correctly
+- [x] JSON schema updated with new fields
+- [x] Schema validation passes
+- [x] Help text accurate
+- [x] All tests pass
+
+## Implementation Approach
+
+See `_techspec.md` "CLI Extension" and `_docs.md` "CLI Documentation" sections.
+
+**Add to root.go or global flags:**
+```go
+--temporal-mode string
+ Temporal server mode (remote or standalone)
+
+--temporal-standalone-database string
+ SQLite database file path (use :memory: for ephemeral)
+
+--temporal-standalone-frontend-port int
+ Frontend service port (default: 7233)
+
+--temporal-standalone-ui-port int
+ Web UI port (default: 8233)
+```
+
+**Schema Updates:**
+Add to `schemas/config.json` under `temporal` object:
+- `mode` (string, enum: ["remote", "standalone"])
+- `standalone` (object with all StandaloneConfig fields)
+
+## Files to Modify
+
+- `cli/root.go` - Add global flags
+- `cli/helpers/global.go` - Wire flags to config
+- `schemas/config.json` - Add schema definitions
+- `cli/help/global-flags.md` - Document new flags
+
+## Tests
+
+- Verify flag precedence: CLI > Env > YAML > Default
+- Test: `compozy start --temporal-mode=standalone`
+- Test: `compozy start --temporal-mode=standalone --temporal-standalone-database=:memory:`
+
+## Validation
+
+```bash
+# Test CLI flags
+compozy start --help | grep temporal
+
+# Verify schema
+make validate-schemas
+
+# Integration test
+compozy start --temporal-mode=standalone --temporal-standalone-database=:memory:
+```
diff --git a/tasks/prd-temporal/_task_08.md b/tasks/prd-temporal/_task_08.md
new file mode 100644
index 00000000..51d13db5
--- /dev/null
+++ b/tasks/prd-temporal/_task_08.md
@@ -0,0 +1,99 @@
+# Task 8.0: Documentation
+
+## status: completed
+
+**Size:** L (2-3 days)
+**Priority:** HIGH - User-facing
+**Dependencies:** Task 5.0
+
+## Overview
+
+Create comprehensive documentation covering temporal modes, architecture, configuration, and troubleshooting.
+
+## Deliverables
+
+- [x] `docs/content/docs/deployment/temporal-modes.mdx` - Mode selection guide
+- [x] `docs/content/docs/architecture/embedded-temporal.mdx` - Architecture deep-dive
+- [x] `docs/content/docs/configuration/temporal.mdx` - Update config reference
+- [x] `docs/content/docs/quick-start/index.mdx` - Update quick start
+- [x] `docs/content/docs/deployment/production.mdx` - Update production guide
+- [x] `docs/content/docs/cli/compozy-start.mdx` - Update CLI docs
+- [x] `docs/content/docs/troubleshooting/temporal.mdx` - Troubleshooting guide
+
+## Acceptance Criteria
+
+- [x] All 7 pages created/updated
+- [x] Navigation config updated
+- [x] Code examples tested
+- [x] YAML configuration examples included
+- [x] Architecture diagrams clear
+- [x] Troubleshooting section comprehensive
+- [x] Production warnings prominent
+- [x] Links validated
+- [x] Docs site builds successfully
+
+## Content Outline
+
+See `_docs.md` for complete content specifications.
+
+**temporal-modes.mdx:**
+- Overview of remote vs standalone
+- When to use each mode
+- Configuration examples
+- Migration guide
+
+**embedded-temporal.mdx:**
+- Four-service architecture
+- SQLite persistence design
+- Port allocation
+- Lifecycle management
+- UI server integration
+
+**Configuration reference updates:**
+- Mode field documentation
+- Standalone config fields
+- Default values
+- Validation rules
+
+**Quick start updates:**
+- Add standalone mode quick start
+- Update setup instructions
+
+**Production guide updates:**
+- Warning against standalone in production
+- Remote mode recommended practices
+
+**CLI documentation:**
+- Document all --temporal-* flags
+- Usage examples
+
+**Troubleshooting:**
+- Port conflicts
+- SQLite errors
+- Startup timeouts
+- UI not accessible
+- Performance issues
+
+## Files to Create/Modify
+
+- `docs/content/docs/deployment/temporal-modes.mdx` (new)
+- `docs/content/docs/architecture/embedded-temporal.mdx` (new)
+- `docs/content/docs/troubleshooting/temporal.mdx` (new)
+- `docs/content/docs/configuration/temporal.mdx` (update)
+- `docs/content/docs/quick-start/index.mdx` (update)
+- `docs/content/docs/deployment/production.mdx` (update)
+- `docs/content/docs/cli/compozy-start.mdx` (update)
+- `docs/source.config.ts` (navigation)
+
+## Validation
+
+```bash
+# Build docs site
+cd docs && npm run build
+
+# Check for broken links
+npm run lint
+
+# Preview locally
+npm run dev
+```
diff --git a/tasks/prd-temporal/_task_09.md b/tasks/prd-temporal/_task_09.md
new file mode 100644
index 00000000..8e2cf552
--- /dev/null
+++ b/tasks/prd-temporal/_task_09.md
@@ -0,0 +1,105 @@
+# Task 9.0: Examples
+
+## status: completed
+
+**Size:** L (2-3 days)
+**Priority:** MEDIUM - User education
+**Dependencies:** Task 5.0
+
+## Overview
+
+Create 7 example projects demonstrating various standalone mode configurations and use cases.
+
+## Deliverables
+
+- [x] `examples/temporal-standalone/basic/` - Basic setup
+- [x] `examples/temporal-standalone/persistent/` - File-based persistence
+- [x] `examples/temporal-standalone/custom-ports/` - Custom port configuration
+- [x] `examples/temporal-standalone/no-ui/` - UI disabled
+- [x] `examples/temporal-standalone/debugging/` - Development with UI
+- [x] `examples/temporal-standalone/migration-from-remote/` - Mode migration
+- [x] `examples/temporal-standalone/integration-testing/` - Testing patterns
+
+## Acceptance Criteria
+
+- [x] All 7 examples created
+- [x] Each example includes README.md with clear instructions
+- [x] Each example includes compozy.yaml configuration
+- [x] Each example includes workflow.yaml
+- [x] All examples tested and working
+- [x] READMEs explain use case and key concepts
+- [x] No linter errors
+
+## Content Outline
+
+See `_examples.md` for complete example specifications.
+
+**basic/:**
+- In-memory mode
+- Default ports
+- UI enabled
+- Simple workflow
+
+**persistent/:**
+- File-based SQLite
+- Demonstrates persistence across restarts
+- Workflow execution before/after restart
+
+**custom-ports/:**
+- Custom FrontendPort (17233)
+- Custom UIPort (18233)
+- Port conflict resolution example
+
+**no-ui/:**
+- EnableUI=false
+- Minimal configuration
+- Suitable for CI/testing
+
+**debugging/:**
+- Full UI enabled
+- Detailed logging
+- Step-by-step debugging workflow
+- UI navigation guide
+
+**migration-from-remote/:**
+- Side-by-side configs (remote vs standalone)
+- Migration checklist
+- Testing strategy
+
+**integration-testing/:**
+- Testing patterns
+- Test fixtures
+- CI configuration example
+- Teardown strategies
+
+## Example Structure
+
+Each example should have:
+```
+examples/temporal-standalone//
+βββ README.md # Clear instructions
+βββ compozy.yaml # Configuration
+βββ workflow.yaml # Example workflow
+βββ .env.example # Environment template (if needed)
+βββ tools/ # Custom tools (if needed)
+ βββ example.ts
+```
+
+## Files to Create
+
+- 7 example directories with complete projects
+- Each with README, configs, and workflow
+
+## Validation
+
+```bash
+# Test each example
+cd examples/temporal-standalone/basic
+compozy start
+
+# Verify workflow execution
+curl -X POST http://localhost:3000/api/workflows/{id}/execute
+
+# Check UI access
+open http://localhost:8233
+```
diff --git a/tasks/prd-temporal/_task_10.md b/tasks/prd-temporal/_task_10.md
new file mode 100644
index 00000000..5ad2cfb0
--- /dev/null
+++ b/tasks/prd-temporal/_task_10.md
@@ -0,0 +1,78 @@
+# Task 10.0: Advanced Integration Tests
+
+## status: completed
+
+**Size:** M (2 days)
+**Priority:** MEDIUM - Edge case coverage
+**Dependencies:** Task 5.0
+
+## Overview
+
+Create advanced integration tests covering error handling, startup lifecycle, and edge cases.
+
+## Deliverables
+
+- [x] `test/integration/temporal/errors_test.go` - Error scenarios
+- [x] `test/integration/temporal/startup_lifecycle_test.go` - Lifecycle edge cases
+
+## Acceptance Criteria
+
+- [x] Port conflict test passes
+- [x] Startup timeout test passes
+- [x] Invalid config rejection test passes
+- [x] Database corruption handling test passes
+- [x] Graceful shutdown under load test passes
+- [x] Concurrent startup/shutdown test passes
+- [x] All tests use `t.Context()`
+- [x] All tests pass
+- [x] No linter errors
+
+## Implementation Approach
+
+See `_tests.md` "Advanced Integration Tests" section.
+
+**errors_test.go:**
+- `TestPortConflict` - Start two servers on same port, expect error
+- `TestStartupTimeout` - Very short timeout, expect deadline exceeded
+- `TestInvalidDatabasePath` - Bad database path, expect error
+- `TestDatabaseCorruption` - Corrupt database file, expect error
+- `TestMissingDatabaseDirectory` - Database in non-existent dir, expect error
+
+**startup_lifecycle_test.go:**
+- `TestGracefulShutdownDuringStartup` - Cancel context during startup
+- `TestMultipleStartCalls` - Start already running server, expect error
+- `TestMultipleStopCalls` - Stop already stopped server, should be idempotent
+- `TestConcurrentRequests` - Multiple workflows during shutdown
+- `TestServerRestartCycle` - Start β Stop β Start β Stop sequence
+
+## Test Patterns
+
+- Use port allocation helpers to avoid conflicts
+- Use `t.TempDir()` for isolation
+- Mock slow startups with context timeouts
+- Test cleanup with `t.Cleanup()`
+- Use table-driven tests for error scenarios
+
+## Files to Create
+
+- `test/integration/temporal/errors_test.go`
+- `test/integration/temporal/startup_lifecycle_test.go`
+
+## Notes
+
+- Error messages must be descriptive and actionable
+- Port conflicts should suggest port configuration
+- Database errors should suggest file permissions check
+- All errors should be wrapped with context
+
+## Validation
+
+```bash
+# Run advanced tests
+gotestsum --format pkgname -- -race -parallel=4 ./test/integration/temporal
+
+# Run full test suite
+make test
+
+# Verify error messages are helpful
+```
diff --git a/tasks/prd-temporal/_tasks.md b/tasks/prd-temporal/_tasks.md
new file mode 100644
index 00000000..1806112f
--- /dev/null
+++ b/tasks/prd-temporal/_tasks.md
@@ -0,0 +1,220 @@
+# Temporal Standalone Mode Implementation Task Summary
+
+## Relevant Files
+
+### Core Implementation Files
+
+- `engine/worker/embedded/config.go` - Embedded server configuration types and validation
+- `engine/worker/embedded/server.go` - Server lifecycle management
+- `engine/worker/embedded/builder.go` - Temporal config builder functions
+- `engine/worker/embedded/namespace.go` - Namespace creation helper
+- `engine/worker/embedded/ui.go` - Web UI server wrapper
+- `pkg/config/config.go` - TemporalConfig.Mode and StandaloneConfig
+- `pkg/config/definition/schema.go` - Configuration registry entries
+- `pkg/config/provider.go` - Configuration defaults
+- `engine/infra/server/dependencies.go` - Server lifecycle integration
+
+### Integration Points
+
+- `engine/worker/client.go` - Temporal client (unchanged, uses overridden HostPort)
+- `engine/worker/mod.go` - Worker initialization (unchanged)
+
+### Test Files
+
+- `engine/worker/embedded/config_test.go` - Config validation tests
+- `engine/worker/embedded/server_test.go` - Server lifecycle tests
+- `engine/worker/embedded/namespace_test.go` - Namespace creation tests
+- `engine/worker/embedded/ui_test.go` - UI server tests
+- `pkg/config/config_test.go` - Config system tests
+- `test/integration/temporal/standalone_test.go` - Core integration tests
+- `test/integration/temporal/mode_switching_test.go` - Mode switching tests
+- `test/integration/temporal/persistence_test.go` - Persistence tests
+- `test/integration/temporal/errors_test.go` - Error handling tests
+- `test/integration/temporal/startup_lifecycle_test.go` - Lifecycle tests
+
+### Documentation Files
+
+- `docs/content/docs/deployment/temporal-modes.mdx` - Mode selection guide
+- `docs/content/docs/architecture/embedded-temporal.mdx` - Architecture deep-dive
+- `docs/content/docs/configuration/temporal.mdx` - Configuration reference
+- `docs/content/docs/quick-start/index.mdx` - Quick start guide
+- `docs/content/docs/deployment/production.mdx` - Production deployment
+- `docs/content/docs/cli/compozy-start.mdx` - CLI documentation
+- `docs/content/docs/troubleshooting/temporal.mdx` - Troubleshooting guide
+
+### Examples
+
+- `examples/temporal-standalone/basic/` - Basic setup example
+- `examples/temporal-standalone/persistent/` - File-based persistence
+- `examples/temporal-standalone/custom-ports/` - Custom port configuration
+- `examples/temporal-standalone/no-ui/` - UI disabled example
+- `examples/temporal-standalone/debugging/` - Debugging with Web UI
+- `examples/temporal-standalone/migration-from-remote/` - Mode migration guide
+- `examples/temporal-standalone/integration-testing/` - Testing example
+
+## Tasks
+
+- [x] 1.0 Embedded Server Package Foundation (L - 3 days)
+- [x] 2.0 Embedded Server Lifecycle (M - 2 days)
+- [x] 3.0 Configuration System Extension (M - 2 days)
+- [x] 4.0 UI Server Implementation (M - 1-2 days)
+- [x] 5.0 Server Lifecycle Integration (M - 2 days)
+- [ ] 6.0 Core Integration Tests (L - 3 days)
+- [x] 7.0 CLI & Schema Updates (S - half day)
+- [x] 8.0 Documentation (L - 2-3 days)
+- [x] 9.0 Examples (L - 2-3 days)
+- [x] 10.0 Advanced Integration Tests (M - 2 days)
+
+## Task Sizing
+
+- S = Small (β€ half-day)
+- M = Medium (1β2 days)
+- L = Large (3+ days)
+
+## Task Design Rules
+
+- Each parent task is a closed deliverable: independently shippable and reviewable
+- Do not split one deliverable across multiple parent tasks; avoid cross-task coupling
+- Each parent task must include unit test subtasks derived from `_tests.md` for this feature
+- Each generated `/_task_.md` must contain explicit Deliverables and Tests sections
+- All runtime code MUST use `logger.FromContext(ctx)` and `config.FromContext(ctx)`
+- Functions MUST be <50 lines per go-coding-standards.mdc
+- Run `make lint && make test` before marking any task as completed
+
+## Execution Plan
+
+```
+CRITICAL PATH (10 days):
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Phase 1: Foundation (3 days) β
+β 1.0 Embedded Server Package Foundation [L] β
+β β β
+β Phase 2: Parallel Core Development (2 days) β
+β ββββββββββββββββββββ ββββββββββββββββββββββββ β
+β β 2.0 Lifecycle [M]β β 3.0 Config System [M]β β
+β ββββββββββ¬ββββββββββ ββββββββββββ¬ββββββββββββ β
+β ββββββββββββββ¬ββββββββββββββ β
+β β β
+β Phase 3: Integration & UI (2 days) β
+β ββββββββββββββββββββββββββββββββββββ β
+β β 5.0 Lifecycle Integration [M] β β
+β ββββββββββββββ¬ββββββββββββββββββββββ β
+β β β
+β Phase 4: Validation (3 days) β
+β 6.0 Core Integration Tests [L] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+PARALLEL LANES (after respective dependencies):
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Lane A: 4.0 UI Server (starts after 2.0) [M, 1-2 days] β
+β Lane B: 7.0 CLI & Schema (starts after 3.0) [S, 0.5 days] β
+β Lane C: 8.0 Documentation (starts after 5.0) [L, 2-3 days] β
+β Lane D: 9.0 Examples (starts after 5.0) [L, 2-3 days] β
+β Lane E: 10.0 Advanced Tests (starts after 5.0) [M, 2 days] β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+**Timeline:**
+- Critical Path: 10 days
+- With Full Parallelization: ~10-11 days total
+- Estimated Effort: ~20 developer-days
+- Can be executed by 2-3 developers working in parallel lanes
+
+**Key Dependencies:**
+- 1.0 blocks everything (foundation)
+- 2.0 and 3.0 can run in parallel after 1.0
+- 5.0 requires both 2.0 and 3.0 complete
+- 6.0, 8.0, 9.0, 10.0 all start after 5.0 (can run in parallel)
+- 4.0 can start as soon as 2.0 completes
+- 7.0 can start as soon as 3.0 completes
+
+## Batch Plan (Grouped Commits)
+
+- [x] **Batch 1 β Foundation:** 1.0
+ - Complete embedded package with tests
+ - Merge as single atomic commit
+
+- [ ] **Batch 2 β Core Systems:** 2.0, 3.0
+ - Server lifecycle + configuration system
+ - Both required for integration
+ - Can be separate PRs, both must merge before Batch 3
+
+- [x] **Batch 3 β Integration:** 4.0, 5.0
+ - UI server + lifecycle integration
+ - Complete end-to-end functionality
+ - Merge together for working standalone mode
+
+- [ ] **Batch 4 β Validation:** 6.0
+ - Core integration tests
+ - Validates Batch 3 works correctly
+ - Required before Batch 6
+
+- [ ] **Batch 5 β Polish:** 7.0, 10.0
+ - CLI support + advanced tests
+ - Quality improvements
+ - Can merge independently
+
+- [ ] **Batch 6 β Documentation:** 8.0, 9.0
+ - Documentation + examples
+ - Can be separate PRs
+ - Should merge before feature release
+
+## Success Criteria
+
+- [ ] All unit tests pass (`make test`)
+- [ ] All integration tests pass
+- [ ] Linter passes (`make lint`)
+- [ ] Workflows execute end-to-end in standalone mode
+- [ ] Web UI accessible at http://localhost:8233
+- [ ] File-based persistence works across restarts
+- [ ] In-memory mode works for ephemeral development
+- [ ] Documentation complete and accurate
+- [ ] Examples all runnable and tested
+- [ ] CLI flags functional
+- [ ] Configuration schema updated
+- [ ] Zero impact on existing remote mode functionality
+
+## Implementation Notes
+
+### Critical Requirements
+
+1. **Use temporal.NewServer()**, NOT Temporalite (deprecated)
+2. **Context-first patterns:** `logger.FromContext(ctx)`, `config.FromContext(ctx)`
+3. **Function length:** Max 50 lines per function
+4. **Error wrapping:** Use `fmt.Errorf("...: %w", err)`
+5. **Resource cleanup:** Always defer cleanup functions
+6. **Port configuration:** Default 7233-7236 for services, 8233 for UI
+
+### Development Environment
+
+- **Go Version:** 1.23+ (project uses 1.25.2)
+- **Dependencies:** `go.temporal.io/server` (latest stable), `go.temporal.io/server/ui-server/v2`
+- **Database:** SQLite (built into Go, no external deps)
+- **Test Commands:**
+ - Scoped tests: `gotestsum --format pkgname -- -race -parallel=4 ./engine/worker/embedded`
+ - Scoped lint: `golangci-lint run --fix --allow-parallel-runners ./engine/worker/embedded/...`
+ - Full validation: `make lint && make test` (run before completing tasks)
+
+### Reference Implementation
+
+- GitHub: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+- Shows proper use of temporal.NewServer() with SQLite, UI server, and namespace creation
+
+## Risk Mitigation
+
+| Risk | Mitigation |
+|------|------------|
+| Large dependency size | Document, acceptable for functionality gained |
+| Port conflicts | Clear error messages, configurable ports (task 3.0) |
+| SQLite corruption | WAL mode, good error messages (task 1.0) |
+| Accidental production use | Validation checks, prominent warnings (tasks 5.0, 8.0) |
+| Startup timeout | Configurable timeout, efficient polling (task 2.0) |
+
+## Questions or Blockers?
+
+If you encounter issues:
+1. Check `_techspec.md` for detailed design
+2. Check `_tests.md` for test requirements
+3. Check `_docs.md` for documentation requirements
+4. Check `.cursor/rules/` for coding standards
+5. Review reference implementation link above
diff --git a/tasks/prd-temporal/_techspec.md b/tasks/prd-temporal/_techspec.md
new file mode 100644
index 00000000..47a3e25d
--- /dev/null
+++ b/tasks/prd-temporal/_techspec.md
@@ -0,0 +1,1002 @@
+# Technical Specification: Temporal Standalone Mode
+
+## Executive Summary
+
+This specification defines the implementation of a standalone/embedded Temporal mode for Compozy using the official `go.temporal.io/server/temporal` package. Unlike the deprecated Temporalite approach, this solution uses the production-grade Temporal server code embedded as a library, configured with SQLite for zero-dependency local development while maintaining the same server capabilities used in production deployments.
+
+**Key Technical Decisions:**
+- Use `temporal.NewServer()` from `go.temporal.io/server/temporal` (official, non-deprecated)
+- SQLite persistence with in-memory or file-based modes
+- Configure all 4 Temporal services (frontend, history, matching, worker)
+- Optional UI server for development debugging
+- Add `Mode` configuration field supporting "remote" (default) and "standalone" modes
+- **Dev/test focus**: Standalone optimized for development, but uses production server code
+
+## System Architecture
+
+### Domain Placement
+
+**New Components:**
+- `engine/worker/embedded/` - Embedded Temporal server wrapper and lifecycle management
+ - `server.go` - Server creation and configuration
+ - `config.go` - Configuration types and validation
+ - `namespace.go` - Namespace initialization helper
+- `pkg/config/config.go` - Extended `TemporalConfig` with mode selection and standalone options
+
+**Modified Components:**
+- `engine/infra/server/dependencies.go` - Server startup sequence to conditionally start embedded Temporal
+- `pkg/config/definition/schema.go` - Registry entries for new configuration fields
+- `pkg/config/provider.go` - Default values for standalone mode
+
+**Unchanged Components:**
+- `engine/worker/client.go` - Client creation remains unchanged (connects via `client.Dial()`)
+- `engine/worker/mod.go` - Worker initialization unchanged
+- All workflow/activity/task execution logic - zero impact
+
+### Component Overview
+
+**Embedded Server Manager (`engine/worker/embedded/server.go`):**
+- Wraps `temporal.NewServer()` with opinionated configuration for local development
+- Configures SQLite persistence (in-memory or file-based)
+- Sets up all 4 services: frontend (7233), history (7234), matching (7235), worker (7236)
+- Creates default namespace automatically
+- Exposes `Start()`, `Stop()`, `FrontendAddress()` methods
+- Handles graceful shutdown and resource cleanup
+
+**UI Server Manager (Optional) (`engine/worker/embedded/ui.go`):**
+- Wraps Temporal UI server for local debugging
+- Connects to embedded Temporal frontend
+- Exposes web UI on configurable port (default: 8233)
+- Can be disabled via configuration
+
+**Configuration Extension (`pkg/config/config.go`):**
+- `TemporalConfig.Mode` - Mode selector ("remote" or "standalone")
+- `TemporalConfig.Standalone` - Standalone-specific configuration
+ - `DatabaseFile` - SQLite path or ":memory:" for ephemeral storage
+ - `FrontendPort` - Frontend service port (default: 7233)
+ - `EnableUI` - Enable web UI (default: true for dev)
+ - `UIPort` - UI server port (default: 8233)
+ - `LogLevel` - Server logging verbosity
+
+**Server Lifecycle Integration (`engine/infra/server/dependencies.go`):**
+- New function: `maybeStartStandaloneTemporal(cfg *config.Config)`
+- Insertion point: BEFORE `maybeStartWorker()` in `setupDependencies()`
+- Dynamically overrides `cfg.Temporal.HostPort` when standalone mode active
+- Registers cleanup function for graceful server shutdown
+
+## Implementation Design
+
+### Core Interfaces
+
+```go
+// engine/worker/embedded/config.go
+package embedded
+
+import "time"
+
+// Config holds embedded Temporal server configuration.
+type Config struct {
+ // DatabaseFile specifies SQLite database location.
+ // Use ":memory:" for ephemeral in-memory storage.
+ // Use file path for persistent storage across restarts.
+ DatabaseFile string
+
+ // FrontendPort is the gRPC port for the frontend service.
+ // Default: 7233
+ FrontendPort int
+
+ // BindIP is the IP address to bind all services to.
+ // Default: "127.0.0.1"
+ BindIP string
+
+ // Namespace is the default namespace to create on startup.
+ // Default: "default"
+ Namespace string
+
+ // ClusterName is the Temporal cluster name.
+ // Default: "compozy-standalone"
+ ClusterName string
+
+ // EnableUI enables the Temporal Web UI server.
+ // Default: true
+ EnableUI bool
+
+ // UIPort is the HTTP port for the Web UI.
+ // Default: 8233
+ UIPort int
+
+ // LogLevel controls server logging verbosity.
+ // Values: "debug", "info", "warn", "error"
+ // Default: "warn"
+ LogLevel string
+
+ // StartTimeout is the maximum time to wait for server startup.
+ // Default: 30s
+ StartTimeout time.Duration
+}
+```
+
+```go
+// engine/worker/embedded/server.go
+package embedded
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "time"
+
+ "go.temporal.io/server/common/config"
+ "go.temporal.io/server/temporal"
+)
+
+// Server wraps an embedded Temporal server instance.
+type Server struct {
+ server temporal.Server
+ uiServer *UIServer // nil if UI disabled
+ config *Config
+ frontendAddr string
+}
+
+// NewServer creates but does not start an embedded Temporal server.
+// Validates configuration and prepares all server components.
+func NewServer(ctx context.Context, cfg *Config) (*Server, error) {
+ if err := validateConfig(cfg); err != nil {
+ return nil, fmt.Errorf("invalid config: %w", err)
+ }
+ applyDefaults(cfg)
+ temporalConfig, err := buildTemporalConfig(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build server config: %w", err)
+ }
+ // Create namespace in SQLite before server start
+ if err := createNamespace(temporalConfig, cfg); err != nil {
+ return nil, fmt.Errorf("failed to create namespace: %w", err)
+ }
+ server, err := temporal.NewServer(
+ temporal.WithConfig(temporalConfig),
+ temporal.ForServices(temporal.DefaultServices),
+ temporal.WithStaticHosts(buildStaticHosts(cfg)),
+ temporal.WithLogger(buildLogger(ctx, cfg)),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temporal server: %w", err)
+ }
+ s := &Server{
+ server: server,
+ config: cfg,
+ frontendAddr: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ }
+ if cfg.EnableUI {
+ s.uiServer = newUIServer(cfg)
+ }
+ return s, nil
+}
+
+// Start starts the embedded Temporal server and optional UI server.
+// Blocks until all services are ready or timeout occurs.
+func (s *Server) Start(ctx context.Context) error {
+ ctx, cancel := context.WithTimeout(ctx, s.config.StartTimeout)
+ defer cancel()
+ if err := s.server.Start(); err != nil {
+ return fmt.Errorf("failed to start temporal server: %w", err)
+ }
+ if err := s.waitForReady(ctx); err != nil {
+ s.server.Stop()
+ return fmt.Errorf("server startup timeout: %w", err)
+ }
+ if s.uiServer != nil {
+ if err := s.uiServer.Start(ctx); err != nil {
+ s.server.Stop()
+ return fmt.Errorf("failed to start UI server: %w", err)
+ }
+ }
+ return nil
+}
+
+// Stop gracefully shuts down the embedded server.
+// Waits for in-flight operations to complete.
+func (s *Server) Stop(ctx context.Context) error {
+ if s.uiServer != nil {
+ s.uiServer.Stop(ctx)
+ }
+ s.server.Stop()
+ return nil
+}
+
+// FrontendAddress returns the gRPC address for Temporal clients.
+// Format: "host:port" (e.g., "127.0.0.1:7233")
+func (s *Server) FrontendAddress() string {
+ return s.frontendAddr
+}
+
+// waitForReady polls the frontend service until ready or timeout.
+func (s *Server) waitForReady(ctx context.Context) error {
+ ticker := time.NewTicker(100 * time.Millisecond)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-ticker.C:
+ conn, err := net.DialTimeout("tcp", s.frontendAddr, 50*time.Millisecond)
+ if err == nil {
+ conn.Close()
+ return nil
+ }
+ }
+ }
+}
+```
+
+```go
+// engine/worker/embedded/builder.go
+package embedded
+
+import (
+ "fmt"
+
+ "github.com/google/uuid"
+ "go.temporal.io/server/common/cluster"
+ "go.temporal.io/server/common/config"
+ "go.temporal.io/server/common/dynamicconfig"
+ "go.temporal.io/server/common/membership/static"
+ "go.temporal.io/server/common/metrics"
+ sqliteplugin "go.temporal.io/server/common/persistence/sql/sqlplugin/sqlite"
+ "go.temporal.io/server/common/primitives"
+)
+
+// buildTemporalConfig creates a temporal.Config from our simplified Config.
+func buildTemporalConfig(cfg *Config) (*config.Config, error) {
+ historyPort := cfg.FrontendPort + 1
+ matchingPort := cfg.FrontendPort + 2
+ workerPort := cfg.FrontendPort + 3
+ metricsPort := cfg.UIPort + 1000
+
+ connectAttrs := buildSQLiteConnectAttrs(cfg.DatabaseFile)
+
+ return &config.Config{
+ Global: config.Global{
+ Metrics: &metrics.Config{
+ Prometheus: &metrics.PrometheusConfig{
+ ListenAddress: fmt.Sprintf("%s:%d", cfg.BindIP, metricsPort),
+ HandlerPath: "/metrics",
+ },
+ },
+ },
+ Persistence: config.Persistence{
+ DefaultStore: "sqlite-default",
+ VisibilityStore: "sqlite-default",
+ NumHistoryShards: 1,
+ DataStores: map[string]config.DataStore{
+ "sqlite-default": {
+ SQL: &config.SQL{
+ PluginName: sqliteplugin.PluginName,
+ ConnectAttributes: connectAttrs,
+ DatabaseName: "temporal",
+ },
+ },
+ },
+ },
+ ClusterMetadata: &cluster.Config{
+ EnableGlobalNamespace: false,
+ FailoverVersionIncrement: 10,
+ MasterClusterName: cfg.ClusterName,
+ CurrentClusterName: cfg.ClusterName,
+ ClusterInformation: map[string]cluster.ClusterInformation{
+ cfg.ClusterName: {
+ Enabled: true,
+ InitialFailoverVersion: 1,
+ RPCAddress: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ ClusterID: uuid.NewString(),
+ },
+ },
+ },
+ DCRedirectionPolicy: config.DCRedirectionPolicy{
+ Policy: "noop",
+ },
+ Services: map[string]config.Service{
+ "frontend": {RPC: config.RPC{GRPCPort: cfg.FrontendPort, BindOnIP: cfg.BindIP}},
+ "history": {RPC: config.RPC{GRPCPort: historyPort, BindOnIP: cfg.BindIP}},
+ "matching": {RPC: config.RPC{GRPCPort: matchingPort, BindOnIP: cfg.BindIP}},
+ "worker": {RPC: config.RPC{GRPCPort: workerPort, BindOnIP: cfg.BindIP}},
+ },
+ Archival: config.Archival{
+ History: config.HistoryArchival{State: "disabled"},
+ Visibility: config.VisibilityArchival{State: "disabled"},
+ },
+ NamespaceDefaults: config.NamespaceDefaults{
+ Archival: config.ArchivalNamespaceDefaults{
+ History: config.HistoryArchivalNamespaceDefaults{State: "disabled"},
+ Visibility: config.VisibilityArchivalNamespaceDefaults{State: "disabled"},
+ },
+ },
+ PublicClient: config.PublicClient{
+ HostPort: fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort),
+ },
+ }, nil
+}
+
+// buildSQLiteConnectAttrs creates connection attributes for SQLite.
+func buildSQLiteConnectAttrs(dbFile string) map[string]string {
+ if dbFile == ":memory:" {
+ return map[string]string{
+ "mode": "memory",
+ "cache": "shared",
+ }
+ }
+ return map[string]string{
+ "_journal_mode": "WAL",
+ "_synchronous": "NORMAL",
+ }
+}
+
+// buildStaticHosts creates static host configuration for all services.
+func buildStaticHosts(cfg *Config) map[primitives.ServiceName]static.Hosts {
+ return map[primitives.ServiceName]static.Hosts{
+ primitives.FrontendService: static.SingleLocalHost(fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort)),
+ primitives.HistoryService: static.SingleLocalHost(fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+1)),
+ primitives.MatchingService: static.SingleLocalHost(fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+2)),
+ primitives.WorkerService: static.SingleLocalHost(fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort+3)),
+ }
+}
+```
+
+### Data Models
+
+**Configuration Extensions:**
+
+```go
+// pkg/config/config.go
+
+type TemporalConfig struct {
+ // Mode controls Temporal connection strategy.
+ // Values: "remote" (default), "standalone"
+ // Production MUST use "remote".
+ Mode string `koanf:"mode" json:"mode" yaml:"mode" mapstructure:"mode" validate:"oneof=remote standalone" env:"TEMPORAL_MODE"`
+
+ // HostPort specifies the Temporal server endpoint for remote mode.
+ // Overridden automatically when Mode="standalone".
+ HostPort string `koanf:"host_port" json:"host_port" yaml:"host_port" mapstructure:"host_port" env:"TEMPORAL_HOST_PORT"`
+
+ Namespace string `koanf:"namespace" json:"namespace" yaml:"namespace" mapstructure:"namespace" env:"TEMPORAL_NAMESPACE"`
+ TaskQueue string `koanf:"task_queue" json:"task_queue" yaml:"task_queue" mapstructure:"task_queue" env:"TEMPORAL_TASK_QUEUE"`
+
+ // Standalone configures the embedded Temporal server.
+ // Only applies when Mode="standalone".
+ Standalone StandaloneConfig `koanf:"standalone" json:"standalone" yaml:"standalone" mapstructure:"standalone"`
+}
+
+type StandaloneConfig struct {
+ // DatabaseFile is the SQLite database path.
+ // Use ":memory:" for ephemeral in-memory storage (default for dev).
+ // Use file path for persistent storage across restarts.
+ DatabaseFile string `koanf:"database_file" json:"database_file" yaml:"database_file" mapstructure:"database_file" env:"TEMPORAL_STANDALONE_DATABASE_FILE"`
+
+ // FrontendPort for Temporal frontend service.
+ // Default: 7233
+ // Other services use FrontendPort+1, +2, +3
+ FrontendPort int `koanf:"frontend_port" json:"frontend_port" yaml:"frontend_port" mapstructure:"frontend_port" validate:"min=0,max=65535" env:"TEMPORAL_STANDALONE_FRONTEND_PORT"`
+
+ // BindIP is the IP address to bind all services to.
+ // Default: "127.0.0.1"
+ BindIP string `koanf:"bind_ip" json:"bind_ip" yaml:"bind_ip" mapstructure:"bind_ip" env:"TEMPORAL_STANDALONE_BIND_IP"`
+
+ // EnableUI enables the Temporal Web UI server.
+ // Default: true (helpful for local debugging)
+ EnableUI bool `koanf:"enable_ui" json:"enable_ui" yaml:"enable_ui" mapstructure:"enable_ui" env:"TEMPORAL_STANDALONE_ENABLE_UI"`
+
+ // UIPort is the HTTP port for the Web UI.
+ // Default: 8233
+ UIPort int `koanf:"ui_port" json:"ui_port" yaml:"ui_port" mapstructure:"ui_port" validate:"min=0,max=65535" env:"TEMPORAL_STANDALONE_UI_PORT"`
+
+ // LogLevel controls Temporal server logging verbosity.
+ // Values: "debug", "info", "warn", "error"
+ // Default: "warn"
+ LogLevel string `koanf:"log_level" json:"log_level" yaml:"log_level" mapstructure:"log_level" validate:"oneof=debug info warn error" env:"TEMPORAL_STANDALONE_LOG_LEVEL"`
+}
+```
+
+**Configuration Registry (`pkg/config/definition/schema.go`):**
+
+```go
+// Register temporal mode field
+registry.Register(&FieldDef{
+ Path: "temporal.mode",
+ Default: "remote",
+ CLIFlag: "temporal-mode",
+ EnvVar: "TEMPORAL_MODE",
+ Type: reflect.TypeOf(""),
+ Help: "Temporal connection mode: remote (production) or standalone (dev/test only)",
+})
+
+// Register standalone config fields
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.database_file",
+ Default: ":memory:",
+ EnvVar: "TEMPORAL_STANDALONE_DATABASE_FILE",
+ Type: reflect.TypeOf(""),
+ Help: "SQLite database path for standalone mode (:memory: or file path)",
+})
+
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.frontend_port",
+ Default: 7233,
+ EnvVar: "TEMPORAL_STANDALONE_FRONTEND_PORT",
+ Type: reflect.TypeOf(0),
+ Help: "Frontend service port for standalone mode",
+})
+
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.bind_ip",
+ Default: "127.0.0.1",
+ EnvVar: "TEMPORAL_STANDALONE_BIND_IP",
+ Type: reflect.TypeOf(""),
+ Help: "IP address to bind standalone Temporal services",
+})
+
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.enable_ui",
+ Default: true,
+ EnvVar: "TEMPORAL_STANDALONE_ENABLE_UI",
+ Type: reflect.TypeOf(true),
+ Help: "Enable Temporal Web UI in standalone mode",
+})
+
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.ui_port",
+ Default: 8233,
+ EnvVar: "TEMPORAL_STANDALONE_UI_PORT",
+ Type: reflect.TypeOf(0),
+ Help: "Web UI port for standalone mode",
+})
+
+registry.Register(&FieldDef{
+ Path: "temporal.standalone.log_level",
+ Default: "warn",
+ EnvVar: "TEMPORAL_STANDALONE_LOG_LEVEL",
+ Type: reflect.TypeOf(""),
+ Help: "Temporal server log level (debug, info, warn, error)",
+})
+```
+
+### API Endpoints
+
+No new API endpoints required. This is a server-side infrastructure change only.
+
+## Integration Points
+
+### External Dependencies
+
+**New Dependencies:**
+1. `go.temporal.io/server` (latest stable version ~v1.25+)
+ - License: MIT
+ - Maintained by Temporal Technologies
+ - Production-grade server code
+ - ~100K+ lines, active development
+ - Used in production by thousands of companies
+
+2. `go.temporal.io/server/ui-server/v2` (optional, for Web UI)
+ - License: MIT
+ - Official Temporal Web UI server
+ - Provides workflow debugging interface
+
+3. `github.com/google/uuid` (transitive, likely already in project)
+ - License: BSD-3-Clause
+ - UUID generation for cluster IDs
+
+**Dependency Justification:**
+- **Why `go.temporal.io/server` instead of Temporalite:**
+ - Temporalite is DEPRECATED (as of late 2023)
+ - This is the official, production-grade Temporal server
+ - Not a "testing-only" library - same code used in production
+ - Active maintenance and development
+ - No migration path needed - this IS the production path
+
+- **Risk Assessment:**
+ - Large dependency (~several MB), but necessary
+ - Well-maintained by Temporal team
+ - Security: Same security model as external Temporal
+ - Performance: SQLite is lightweight for dev/test
+
+## Impact Analysis
+
+| Affected Component | Type of Impact | Description & Risk Level | Required Action |
+|--------------------|----------------|--------------------------|-----------------|
+| `pkg/config/config.go` | Schema Extension | Add Mode, Standalone fields. Non-breaking (new optional fields). Low risk. | Update struct, add validation |
+| `pkg/config/definition/schema.go` | Registry Extension | Register new config fields. Low risk. | Add FieldDef entries |
+| `pkg/config/provider.go` | Defaults Extension | Add standalone defaults. Low risk. | Add to defaults map |
+| `engine/infra/server/dependencies.go` | Startup Sequence Modification | Insert embedded server startup before worker initialization. Medium risk (startup path). | Add conditional logic |
+| `engine/worker/embedded/` (NEW) | New Package | Temporal server wrapper. No risk to existing code. | Create package |
+| `go.mod` | Dependency Addition | Add `go.temporal.io/server`. Large dep (~several MB). Low risk. | `go get go.temporal.io/server` |
+| `cli/cmd/start/start.go` | Documentation | Add CLI flag for `--temporal-mode`. Low risk. | Add flag, update help |
+| `schemas/config.json` | Schema Extension | Extend Temporal schema with new fields. Low risk. | Add properties to JSON Schema |
+| `docs/content/` | Documentation | Document standalone mode usage. Zero risk. | Create/update docs |
+
+**Critical Path Dependencies:**
+- No database schema changes
+- No API contract changes
+- Zero impact on workflow/activity execution logic
+- Backward compatible: existing configurations continue to work (Mode defaults to "remote")
+
+## Testing Approach
+
+### Unit Tests
+
+**`engine/worker/embedded/server_test.go`:**
+- Should create server with valid config
+- Should start and stop server successfully
+- Should reject invalid config (bad port, bad log level, bad database path)
+- Should return valid FrontendAddress after start
+- Should create default namespace on startup
+- Should handle start errors gracefully (port in use)
+- Should handle stop errors gracefully
+- Should timeout if server doesn't start within StartTimeout
+
+**`engine/worker/embedded/config_test.go`:**
+- Should validate Config fields
+- Should apply defaults correctly
+- Should build SQLite connect attributes for memory mode
+- Should build SQLite connect attributes for file mode
+- Should create valid Temporal config structure
+
+**`pkg/config/config_test.go`:**
+- Should validate Mode field (only "remote" or "standalone")
+- Should validate Standalone.FrontendPort range
+- Should validate Standalone.UIPort range
+- Should validate Standalone.LogLevel values
+- Should provide defaults for Standalone fields
+- Should handle missing Standalone config when Mode="standalone"
+
+### Integration Tests
+
+**`test/integration/temporal/standalone_test.go`:**
+- Should start Compozy server in standalone mode with memory persistence
+- Should start Compozy server in standalone mode with file persistence
+- Should execute simple workflow end-to-end
+- Should persist workflows across server restarts (file-based SQLite)
+- Should isolate namespaces correctly
+- Should handle worker registration
+- Should shut down cleanly without errors
+- Should expose Web UI when EnableUI=true
+- Should not expose Web UI when EnableUI=false
+
+**`test/integration/temporal/mode_switching_test.go`:**
+- Should start in remote mode (default)
+- Should start in standalone mode when configured
+- Should fail gracefully if embedded server fails to start
+- Should fail gracefully if port is already in use
+- Should log appropriate warnings about standalone limitations
+- Should override HostPort when standalone mode active
+
+**`test/integration/temporal/startup_lifecycle_test.go`:**
+- Should start embedded server before worker
+- Should wait for server readiness before proceeding
+- Should clean up embedded server on shutdown
+- Should handle server startup timeout
+- Should handle concurrent startup/shutdown
+
+### Test Data Requirements
+
+**Fixtures (`test/integration/temporal/testdata/`):**
+- `compozy-standalone-memory.yaml` - Config with in-memory mode
+- `compozy-standalone-file.yaml` - Config with file-based persistence
+- `compozy-standalone-no-ui.yaml` - Config with UI disabled
+- `compozy-remote.yaml` - Remote mode config (existing)
+- `simple-workflow.yaml` - Minimal workflow for testing
+
+## Development Sequencing
+
+### Build Order
+
+1. **Embedded Server Package (First)**
+ - Create `engine/worker/embedded/` package structure
+ - Implement `Config` struct with validation
+ - Implement `Server` struct with lifecycle management
+ - Implement helper functions (`buildTemporalConfig`, `buildStaticHosts`, etc.)
+ - Implement namespace creation helper
+ - Add unit tests for server lifecycle
+ - **Why first:** Core functionality; can be developed and tested independently
+
+2. **Configuration Foundation (Second)**
+ - Update `pkg/config/config.go` with Mode and Standalone fields
+ - Add registry entries in `pkg/config/definition/schema.go`
+ - Add defaults in `pkg/config/provider.go`
+ - Add config validation tests
+ - **Dependencies:** Requires (1) for embedded Config types
+
+3. **Server Lifecycle Integration (Third)**
+ - Modify `engine/infra/server/dependencies.go`
+ - Add `maybeStartStandaloneTemporal()` function
+ - Wire cleanup into existing cleanup chain
+ - Add integration tests for startup/shutdown
+ - **Dependencies:** Requires (1) and (2)
+
+4. **Optional UI Server (Fourth)**
+ - Implement `engine/worker/embedded/ui.go`
+ - Add UI server lifecycle management
+ - Add tests for UI server
+ - **Dependencies:** Requires (1); can be done in parallel with (3)
+
+5. **CLI and Documentation (Fifth)**
+ - Add `--temporal-mode` CLI flag
+ - Update help text and examples
+ - Update JSON Schema
+ - **Dependencies:** Requires (1), (2), (3) for full context
+
+6. **Integration Tests and Examples (Sixth)**
+ - Implement end-to-end tests with standalone mode
+ - Test both memory and file-based persistence
+ - Create example projects demonstrating standalone mode
+ - **Dependencies:** Requires working implementation from (1)-(5)
+
+### Technical Dependencies
+
+**Blocking Dependencies:**
+- Go 1.23+ (already satisfied)
+- `go.temporal.io/server` compatible with project's Temporal SDK version
+- SQLite support in host environment (universally available)
+
+**Non-Blocking:**
+- Documentation updates can proceed in parallel with implementation
+- CLI flag additions can be done independently after config foundation
+
+## Monitoring & Observability
+
+### Metrics (via existing `infra/monitoring` package)
+
+**New Metrics:**
+```go
+// Standalone mode status
+compozy_temporal_standalone_enabled{mode="memory|file"} gauge
+// Server lifecycle
+compozy_temporal_standalone_starts_total counter
+compozy_temporal_standalone_stops_total counter
+compozy_temporal_standalone_errors_total{type="start|stop|namespace"} counter
+// Server health
+compozy_temporal_standalone_ready gauge
+// Database stats (file mode only)
+compozy_temporal_standalone_db_size_bytes gauge
+// UI server status
+compozy_temporal_standalone_ui_enabled gauge
+```
+
+### Logging (via existing `pkg/logger` package)
+
+**Key Log Events:**
+- `Info`: "Starting embedded Temporal server" (mode=standalone, database=:memory:|path, ui_enabled=true|false)
+- `Info`: "Embedded Temporal server started successfully" (frontend_addr=..., ui_addr=..., duration=...)
+- `Info`: "Created Temporal namespace" (namespace=..., cluster=...)
+- `Warn`: "Standalone mode active - optimized for development, not production"
+- `Error`: "Failed to start embedded Temporal server" (error=..., phase=config|namespace|server|ui)
+- `Error`: "Embedded server startup timeout" (timeout=..., phase=...)
+- `Info`: "Stopping embedded Temporal server"
+- `Info`: "Embedded Temporal server stopped" (duration=...)
+- `Debug`: "Embedded Temporal server lifecycle" (step=create|start|wait|ready|stop)
+- `Debug`: "Temporal server configuration" (services=4, frontend_port=..., persistence=...)
+
+### Grafana Dashboards
+
+**Extend existing Temporal dashboard:**
+- Add "Standalone Mode" section
+- Display mode gauge (remote vs standalone)
+- Show standalone-specific error rates
+- Display startup/stop metrics
+- Show database file size (file mode)
+- Link to Temporal Web UI when in standalone mode (if enabled)
+
+## Technical Considerations
+
+### Key Decisions
+
+**Decision 1: Use `temporal.NewServer()` vs Temporalite**
+- **Rationale:**
+ - Temporalite is officially DEPRECATED (as of late 2023/early 2024)
+ - `temporal.NewServer()` is the production-grade approach
+ - Same server code used in production deployments
+ - Active maintenance and long-term support
+ - No migration path needed - this IS the production implementation
+- **Trade-offs:**
+ - Larger dependency (~several MB vs Temporalite's lighter footprint)
+ - More configuration complexity (but more control)
+ - Requires understanding Temporal server architecture
+- **Alternatives Rejected:**
+ - Temporalite (deprecated, no future)
+ - Docker-in-Docker (fragile, requires Docker, slow)
+ - TestContainers (requires Docker, not suitable for dev mode)
+ - `temporaltest` package (test-only, lacks persistence, missing features)
+
+**Decision 2: SQLite Persistence Only**
+- **Rationale:**
+ - Embedded server use case targets development/testing
+ - SQLite is zero-dependency, built into Go
+ - Supports both in-memory (fast, ephemeral) and file-based (persistent)
+ - Same persistence backend that Temporal server supports
+- **Trade-offs:**
+ - Limited scalability (acceptable for dev/test)
+ - Single-node only (acceptable for dev/test)
+ - File-based mode requires disk I/O
+- **Alternatives Rejected:**
+ - Postgres/MySQL (too heavy for embedded use case)
+ - In-memory only (lose persistence across restarts)
+
+**Decision 3: Optional Web UI Server**
+- **Rationale:**
+ - Significantly improves debugging experience in local development
+ - Temporal Web UI is the official debugging tool
+ - Lightweight HTTP server, minimal overhead
+ - Can be disabled if not needed
+- **Trade-offs:**
+ - Additional port required (default 8233)
+ - Slight increase in startup time
+ - More complexity in lifecycle management
+- **Alternatives Rejected:**
+ - Always enable UI (users may not need it, wastes resources)
+ - Never enable UI (poor debugging experience)
+ - External UI server (defeats purpose of standalone mode)
+
+**Decision 4: Four-Service Architecture**
+- **Rationale:**
+ - Mirrors production Temporal deployment
+ - Frontend (7233), History (7234), Matching (7235), Worker (7236)
+ - Allows understanding production architecture in dev
+ - Enables proper service isolation and testing
+- **Trade-offs:**
+ - Requires 4 ports (but all on localhost)
+ - More complex than single-service
+ - Slightly higher resource usage
+- **Alternatives Rejected:**
+ - Single-service (doesn't reflect production architecture)
+ - Two-service (insufficient for realistic scenarios)
+
+### Known Risks
+
+**Risk 1: `go.temporal.io/server` Version Compatibility**
+- **Description:** Temporal server package may have breaking changes between versions
+- **Likelihood:** Low (Temporal maintains strong versioning discipline)
+- **Impact:** High (could break embedded server on upgrade)
+- **Mitigation:**
+ - Pin exact version in `go.mod`
+ - Test thoroughly before upgrading Temporal dependencies
+ - Monitor Temporal release notes for breaking changes
+ - Consider vendoring if stability critical
+
+**Risk 2: Port Conflicts in Standalone Mode**
+- **Description:** Ports 7233-7236 (and 8233 for UI) may be in use
+- **Likelihood:** Medium (developers may have other services running)
+- **Impact:** Medium (server fails to start, clear error message needed)
+- **Mitigation:**
+ - Provide clear error messages with resolution steps
+ - Allow port configuration via config
+ - Document common port conflicts
+ - Integration tests for port conflict scenarios
+
+**Risk 3: SQLite Database Corruption (File Mode)**
+- **Description:** Improper shutdown or system crash may corrupt SQLite file
+- **Likelihood:** Low (WAL mode reduces risk)
+- **Impact:** Low to Medium (workflow state lost, must recreate)
+- **Mitigation:**
+ - Use WAL (Write-Ahead Logging) mode by default
+ - Document backup procedures for important workflows
+ - Recommend in-memory mode for disposable development
+ - Provide clear error messages on corruption
+
+**Risk 4: Standalone Mode Used in Production Accidentally**
+- **Description:** Developer/operator sets `mode: standalone` in production config
+- **Likelihood:** Medium (human error, copy/paste configs)
+- **Impact:** Critical (single point of failure, data loss, performance issues)
+- **Mitigation:**
+ - Log PROMINENT WARNING on startup when standalone mode active
+ - Check `RUNTIME_ENVIRONMENT` and fail if standalone + production
+ - Document clearly in all examples and docs
+ - Add validation that warns/fails in production-like environments
+
+**Risk 5: Large `go.temporal.io/server` Dependency Size**
+- **Description:** Temporal server package is large (~10-20MB compressed)
+- **Likelihood:** Certain (inherent to approach)
+- **Impact:** Low (longer initial download, larger binary)
+- **Mitigation:**
+ - Document dependency size in README
+ - Binary size is acceptable trade-off for functionality
+ - Consider lazy loading if becomes issue (future optimization)
+
+### Special Requirements
+
+**Performance:**
+- Standalone mode startup must complete within 30 seconds (configurable via `StartTimeout`)
+- Server readiness check must poll efficiently (100ms intervals)
+- No performance degradation for remote mode (standalone code path only active when Mode="standalone")
+- SQLite file size monitoring and logging
+- Web UI must be responsive (<500ms page loads)
+
+**Security:**
+- Standalone mode uses localhost-only binding by default (BindIP: "127.0.0.1")
+- No authentication required (acceptable for local development)
+- SQLite file permissions: restrict to owner (0600) when using file-based persistence
+- Disable archival (reduces attack surface)
+- Log warnings if standalone mode configured with non-localhost BindIP
+
+**Operability:**
+- Clear error messages if server fails to start (port conflicts, permission issues)
+- Graceful shutdown with timeout
+- CLI must show standalone mode status in `compozy config show`
+- Web UI link logged on startup when enabled
+- Health checks for server readiness
+- Prometheus metrics endpoint exposed
+
+### Standards Compliance
+
+This implementation follows:
+
+- **architecture.mdc:**
+ - Clean separation between server lifecycle (infra) and client (worker)
+ - Domain-driven structure with `engine/worker/embedded/` package
+ - Infrastructure concerns properly isolated
+
+- **go-coding-standards.mdc:**
+ - Context propagation: `logger.FromContext(ctx)`, `config.FromContext(ctx)`
+ - Error wrapping: `fmt.Errorf("...: %w", err)`
+ - Function length: All functions <50 lines (split into helpers)
+ - Constructor pattern: `NewServer(ctx, cfg)` with validation
+ - Resource cleanup: Defer cleanup functions
+ - No global state or singletons
+
+- **global-config.mdc:**
+ - Configuration via registry with defaults, env vars, validation
+ - Context-based config access
+ - Precedence: defaults β YAML β env β CLI
+
+- **backwards-compatibility.mdc:**
+ - No backwards compatibility required (project in alpha)
+ - Greenfield approach - focus on best design
+
+- **test-standards.mdc:**
+ - `t.Run("Should...")` pattern
+ - Use `t.Context()` instead of `context.Background()`
+ - Testify assertions
+ - Integration tests in `test/integration/`
+ - Table-driven tests for validation
+
+- **logger-config.mdc:**
+ - Context-first logging
+ - `logger.FromContext(ctx)` pattern
+ - No logger parameters or DI
+ - Structured logging with key-value pairs
+
+## Libraries Assessment
+
+### Build vs Buy Decision
+
+**Decision: BUY (Use `go.temporal.io/server`)**
+
+**Primary Candidate: Temporal Server**
+- **Repository:** https://github.com/temporalio/temporal
+- **Package:** `go.temporal.io/server`
+- **License:** MIT
+- **Maintenance:** Active (Temporal Technologies official project)
+- **Stars/Adoption:** 13K+ GitHub stars, thousands of production deployments
+- **Integration Fit:** Perfect - official production server code embedded as library
+- **Performance:** Production-grade, handles thousands of workflows/sec
+- **Security:** Active security team, CVE monitoring, regular updates
+- **Documentation:** Comprehensive official docs, active community
+- **Pros:**
+ - Official Temporal implementation - first-party support
+ - NOT DEPRECATED (unlike Temporalite)
+ - Production-grade code - same code used by large enterprises
+ - Active development and maintenance
+ - Comprehensive feature set (all Temporal capabilities)
+ - Can scale from dev to production (same code path)
+ - Well-documented configuration
+ - Strong type safety and API design
+- **Cons:**
+ - Large dependency (~10-20MB)
+ - Complex configuration (but provides full control)
+ - Requires understanding Temporal architecture
+ - Higher resource usage than lightweight alternatives (but acceptable for dev/test)
+
+**Alternatives Considered:**
+
+1. **Temporalite** (`github.com/temporalio/temporalite`)
+ - **Status:** DEPRECATED (as of late 2023/early 2024)
+ - **Pros:** Lightweight, simpler API
+ - **Cons:** No future, deprecated, no maintenance, migration required
+ - **Rejected:** Cannot use deprecated library for new feature
+
+2. **`temporaltest` Package** (`go.temporal.io/sdk/testsuite`)
+ - **Pros:** Built into SDK, lightweight
+ - **Cons:** Test-only, no persistence, missing features (UI, metrics), not suitable for dev mode
+ - **Rejected:** Too limited for development use case
+
+3. **Build Custom Embedded Server**
+ - **Pros:** Full control, no external dependency
+ - **Cons:** Months of development, requires deep Temporal internals knowledge, high maintenance burden, security concerns
+ - **Rejected:** Not feasible; would require 6-12 months of senior engineering time
+
+4. **Docker-in-Docker**
+ - **Pros:** Uses official Temporal Docker image
+ - **Cons:** Requires Docker, slow startup (~10-30 seconds), fragile in CI, complex lifecycle
+ - **Rejected:** Poor developer experience, defeats purpose of "standalone"
+
+5. **TestContainers**
+ - **Pros:** Well-known testing library
+ - **Cons:** Requires Docker, test-only (not suitable for dev mode), slow startup
+ - **Rejected:** Doesn't solve local development problem
+
+**Final Recommendation:** Use `go.temporal.io/server/temporal.NewServer()`. It's the official, production-grade, non-deprecated solution. While the dependency is large, it provides the full Temporal feature set and is actively maintained by Temporal Technologies. The code is the same used in production deployments, ensuring a realistic development environment.
+
+## Risk & Assumptions Registry
+
+| Risk | Likelihood | Impact | Mitigation | Status |
+|------|------------|--------|------------|--------|
+| Server version compatibility breaks on upgrade | Low | High | Pin versions, test before upgrade | Open |
+| Port conflicts prevent startup | Medium | Medium | Clear errors, configurable ports | Open |
+| SQLite corruption (file mode) | Low | Medium | WAL mode, document backups | Open |
+| Accidental production use | Medium | Critical | Validation checks, warnings, docs | Open |
+| Large dependency size | Certain | Low | Document, acceptable trade-off | Accepted |
+| Startup timeout in slow environments | Low | Low | Configurable timeout, efficient polling | Open |
+| Web UI security exposure | Low | Low | Localhost-only by default | Open |
+
+**Assumptions:**
+1. Standalone mode is used exclusively for development and testing
+2. Developers have SQLite support (universally available with Go)
+3. Temporal SDK and server versions remain compatible (same organization maintains both)
+4. Network ports 7233-7236 (and 8233 for UI) are available or configurable
+5. File system write permissions available for SQLite file (when file-based persistence used)
+6. Localhost binding is acceptable (BindIP can be configured if needed)
+7. 30-second startup timeout is sufficient (configurable if needed)
+8. Developers accept larger binary size for embedded server functionality
+
+## Planning Artifacts (Must Be Generated)
+
+This technical specification must be accompanied by:
+
+1. **Documentation Plan** (`tasks/prd-temporal/_docs.md`)
+ - User-facing documentation updates
+ - Configuration reference
+ - Quick start guides
+ - Migration guides
+
+2. **Examples Plan** (`tasks/prd-temporal/_examples.md`)
+ - Example projects demonstrating standalone mode
+ - Configuration templates
+ - Common use cases
+
+3. **Tests Plan** (`tasks/prd-temporal/_tests.md`)
+ - Unit test coverage requirements
+ - Integration test scenarios
+ - Performance test criteria
+
+## Next Steps
+
+1. Review and approve this technical specification
+2. Create implementation tasks from build order sequence
+3. Set up development branch for Temporal standalone feature
+4. Begin implementation with embedded server package (Step 1 of build order)
+5. Iterate with tests at each stage to validate design decisions
+6. Create PRD cleanup document if any technical content was misplaced in PRD
+
+---
+
+**Document Metadata:**
+- **Author:** Technical Specification Agent
+- **Date:** 2025-01-27 (Revised)
+- **Status:** Draft - Awaiting Review
+- **Approach:** `temporal.NewServer()` (Official, Non-Deprecated)
+- **Reference:** https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+- **Dependencies:** tasks/prd-temporal/_prd.md (if exists)
+- **Related Artifacts:**
+ - tasks/prd-temporal/_docs.md (to be created)
+ - tasks/prd-temporal/_examples.md (to be created)
+ - tasks/prd-temporal/_tests.md (to be created)
+
+## Appendix: Code References
+
+**Based on:** https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+
+**Key Patterns from Reference:**
+1. SQLite configuration with in-memory mode
+2. Four-service architecture setup
+3. Namespace creation via `sqliteschema.CreateNamespaces()`
+4. UI server integration
+5. Static host configuration
+6. Prometheus metrics endpoint
diff --git a/tasks/prd-temporal/_tests.md b/tasks/prd-temporal/_tests.md
new file mode 100644
index 00000000..96e520e7
--- /dev/null
+++ b/tasks/prd-temporal/_tests.md
@@ -0,0 +1,443 @@
+# Tests Plan: Temporal Standalone Mode
+
+## Guiding Principles
+
+- Follow `.cursor/rules/test-standards.mdc` and project rules
+- Use `t.Run("Should β¦")` pattern, testify assertions, context helpers
+- Test both in-memory and file-based persistence
+- Test Web UI server lifecycle
+- Verify four-service architecture
+- No mocks for Temporal server itself (real embedded instance)
+
+## Coverage Matrix
+
+Map PRD/Tech Spec requirements to concrete test files:
+
+| Requirement | Test File | Test Cases |
+|-------------|-----------|------------|
+| Embedded server starts successfully | `engine/worker/embedded/server_test.go` | TestNewServer, TestServerStart |
+| Configuration validation | `engine/worker/embedded/config_test.go` | TestValidateConfig, TestApplyDefaults |
+| SQLite in-memory mode | `test/integration/temporal/standalone_test.go` | TestStandaloneMemoryMode |
+| SQLite file-based mode | `test/integration/temporal/standalone_test.go` | TestStandaloneFileMode |
+| Web UI server | `engine/worker/embedded/ui_test.go` | TestUIServerStart, TestUIServerStop |
+| Mode selection | `test/integration/temporal/mode_switching_test.go` | TestDefaultModeIsRemote, TestStandaloneModeActivation |
+| Port configuration | `test/integration/temporal/standalone_test.go` | TestStandaloneCustomPorts |
+| Workflow execution | `test/integration/temporal/standalone_test.go` | TestStandaloneWorkflowExecution |
+| Persistence across restarts | `test/integration/temporal/persistence_test.go` | TestStandalonePersistence |
+| Graceful shutdown | `engine/worker/embedded/server_test.go` | TestServerStop |
+| Error handling | `test/integration/temporal/errors_test.go` | TestStartupFailure, TestPortConflict |
+| Config from context | `pkg/config/config_test.go` | TestTemporalConfigValidation |
+
+## Unit Tests
+
+### engine/worker/embedded/config_test.go
+- **Should validate required fields**
+ - Empty DatabaseFile with file mode β error
+ - Invalid FrontendPort (negative, >65535) β error
+ - Invalid UIPort (negative, >65535) β error
+ - Invalid LogLevel (not debug|info|warn|error) β error
+ - Invalid BindIP (malformed) β error
+
+- **Should apply defaults correctly**
+ - Nil config β defaults applied
+ - Partial config β missing fields get defaults
+ - FrontendPort default = 7233
+ - BindIP default = "127.0.0.1"
+ - EnableUI default = true
+ - UIPort default = 8233
+ - LogLevel default = "warn"
+ - StartTimeout default = 30s
+
+- **Should build SQLite connect attributes**
+ - ":memory:" β mode=memory, cache=shared
+ - File path β _journal_mode=WAL, _synchronous=NORMAL
+ - Valid attributes map returned
+
+- **Should build static hosts configuration**
+ - Frontend = BindIP:FrontendPort
+ - History = BindIP:FrontendPort+1
+ - Matching = BindIP:FrontendPort+2
+ - Worker = BindIP:FrontendPort+3
+
+### engine/worker/embedded/server_test.go
+- **Should create server with valid config**
+ - Valid config β server instance created
+ - Server not yet started
+ - FrontendAddress returns correct value
+
+- **Should reject invalid config**
+ - Invalid port range β error
+ - Invalid log level β error
+ - Bad database file path (no permissions) β error
+
+- **Should start server successfully**
+ - Call Start() β no error
+ - Server accepts connections on frontend port
+ - Services respond to health checks
+ - Namespace created
+
+- **Should timeout if server doesn't start**
+ - Very short StartTimeout β context.DeadlineExceeded
+ - Server resources cleaned up
+
+- **Should stop server gracefully**
+ - Call Stop() after Start() β no error
+ - Services shut down cleanly
+ - Ports released
+
+- **Should handle port conflicts**
+ - Port already in use β descriptive error
+ - Error message includes port number
+ - Error message suggests resolution
+
+- **Should wait for ready state**
+ - waitForReady() polls until frontend accessible
+ - Returns nil when ready
+ - Returns error on timeout
+
+### engine/worker/embedded/namespace_test.go
+- **Should create namespace in SQLite**
+ - Namespace created before server start
+ - Namespace queryable via Temporal client
+ - Default namespace = "default"
+ - Custom namespace supported
+
+- **Should handle namespace creation errors**
+ - Invalid namespace name β error
+ - Database connection failure β error
+
+### engine/worker/embedded/ui_test.go
+- **Should create UI server when enabled**
+ - EnableUI=true β UI server created
+ - UI server not started yet
+
+- **Should start UI server**
+ - Call Start() β no error
+ - HTTP server accessible on UIPort
+ - Web UI serves pages
+
+- **Should not create UI server when disabled**
+ - EnableUI=false β UI server is nil
+ - Start() skips UI server
+
+- **Should stop UI server gracefully**
+ - Call Stop() β no error
+ - HTTP server shuts down
+ - Port released
+
+### pkg/config/config_test.go
+- **Should validate TemporalConfig.Mode**
+ - Mode="remote" β valid
+ - Mode="standalone" β valid
+ - Mode="invalid" β validation error
+ - Empty Mode β defaults to "remote"
+
+- **Should validate StandaloneConfig fields**
+ - Valid FrontendPort range
+ - Valid UIPort range
+ - Valid LogLevel enum
+ - Valid BindIP format
+
+- **Should provide defaults for Standalone**
+ - Empty StandaloneConfig β defaults applied
+ - DatabaseFile default = ":memory:"
+ - FrontendPort default = 7233
+ - EnableUI default = true
+
+## Integration Tests
+
+### test/integration/temporal/standalone_test.go
+- **Should start Compozy server in standalone mode with memory persistence**
+ - Set Mode="standalone", DatabaseFile=":memory:"
+ - Server starts successfully
+ - Temporal client connects to embedded server
+ - Worker registers workflows
+ - Execute simple workflow end-to-end
+ - Workflow completes successfully
+ - Server shuts down cleanly
+
+- **Should start Compozy server in standalone mode with file persistence**
+ - Set DatabaseFile="./test-temporal.db"
+ - Server starts successfully
+ - Database file created
+ - Workflow executes
+ - Database file contains workflow history
+
+- **Should execute workflows end-to-end**
+ - Start server, trigger workflow
+ - Workflow transitions through states
+ - Activities execute
+ - Workflow completes
+ - Result retrievable via client
+
+- **Should isolate namespaces correctly**
+ - Create workflows in namespace "test-1"
+ - Create workflows in namespace "test-2"
+ - Workflows isolated per namespace
+ - No cross-namespace contamination
+
+- **Should handle worker registration**
+ - Worker registers with embedded server
+ - Worker polls task queue
+ - Worker executes workflows
+ - Multiple workers supported
+
+- **Should expose Web UI when enabled**
+ - EnableUI=true
+ - HTTP request to UIPort β 200 OK
+ - Web UI serves HTML
+
+- **Should not expose Web UI when disabled**
+ - EnableUI=false
+ - HTTP request to UIPort β connection refused
+
+- **Should support custom ports**
+ - FrontendPort=8233, UIPort=9233
+ - Server binds to custom ports
+ - Client connects to custom frontend port
+ - Web UI accessible on custom UI port
+
+- **Should shut down cleanly**
+ - Server.Stop() called
+ - All services stop
+ - Ports released
+ - No goroutine leaks
+
+### test/integration/temporal/mode_switching_test.go
+- **Should start in remote mode by default**
+ - No Mode specified β defaults to "remote"
+ - Server attempts to connect to HostPort
+ - (Test may mock or skip actual remote connection)
+
+- **Should start in standalone mode when configured**
+ - Mode="standalone"
+ - Embedded server starts
+ - HostPort overridden to embedded server address
+
+- **Should fail gracefully if embedded server fails to start**
+ - Simulate startup failure (port in use)
+ - Clear error message
+ - Server startup aborted
+ - Resources cleaned up
+
+- **Should fail gracefully if port already in use**
+ - Occupy port 7233
+ - Attempt to start standalone mode
+ - Error message mentions port conflict
+ - Suggests resolution (change port or free up port)
+
+- **Should log warnings about standalone limitations**
+ - Mode="standalone"
+ - Log includes: "Standalone mode active - optimized for development, not production"
+ - Log includes: "Using embedded Temporal server"
+
+- **Should override HostPort when standalone mode active**
+ - Config has HostPort="remote-server:7233"
+ - Mode="standalone"
+ - HostPort dynamically set to "127.0.0.1:7233"
+ - Client connects to embedded server, not remote
+
+### test/integration/temporal/persistence_test.go
+- **Should persist workflows across server restarts (file-based SQLite)**
+ - Start server with DatabaseFile="./test-temporal.db"
+ - Trigger workflow A
+ - Stop server
+ - Restart server with same DatabaseFile
+ - Workflow A history still accessible
+ - Trigger workflow B
+ - Both A and B in history
+
+- **Should not persist workflows in memory mode**
+ - Start server with DatabaseFile=":memory:"
+ - Trigger workflow
+ - Stop server
+ - Restart server with DatabaseFile=":memory:"
+ - Previous workflow history NOT present (ephemeral)
+
+- **Should use WAL mode for file-based SQLite**
+ - DatabaseFile="./test-temporal.db"
+ - WAL files present: .db-shm, .db-wal
+ - Better concurrency and reliability
+
+- **Should handle database cleanup on stop**
+ - File-based mode: database file remains
+ - In-memory mode: database destroyed on stop
+
+### test/integration/temporal/startup_lifecycle_test.go
+- **Should start embedded server before worker**
+ - Monitor startup sequence
+ - Embedded server starts first
+ - Worker initialization waits for server ready
+ - Worker connects to embedded server
+
+- **Should wait for server readiness before proceeding**
+ - Server starts asynchronously
+ - waitForReady() polls until services available
+ - Timeout if services don't become ready
+
+- **Should clean up embedded server on shutdown**
+ - Server shutdown sequence
+ - Embedded server stops before other cleanup
+ - Ports released
+ - Resources freed
+
+- **Should handle server startup timeout**
+ - Set very short StartTimeout
+ - Server doesn't become ready in time
+ - Startup fails with timeout error
+ - Cleanup executed
+
+- **Should handle concurrent startup/shutdown**
+ - Simulate rapid start/stop cycles
+ - No race conditions
+ - Resources properly managed
+
+### test/integration/temporal/errors_test.go
+- **Should handle port conflicts gracefully**
+ - Occupy frontend port (7233)
+ - Attempt to start standalone mode
+ - Error: "failed to start temporal server: port 7233 already in use"
+ - Resolution suggested in error message
+
+- **Should handle database errors**
+ - Invalid database file path
+ - No write permissions for database file
+ - Database corruption
+ - Clear error messages for each scenario
+
+- **Should handle namespace creation errors**
+ - Simulate namespace creation failure
+ - Server startup aborted
+ - Clear error message
+
+- **Should handle UI server errors**
+ - UI port conflict
+ - UI server fails to start
+ - Temporal server still starts (UI is optional failure)
+ - Warning logged
+
+## Fixtures & Testdata
+
+Add under `engine/worker/embedded/testdata/`:
+- `valid-config.yaml` - Valid standalone configuration
+- `invalid-config.yaml` - Invalid configuration for error testing
+- `custom-ports.yaml` - Custom port configuration
+
+Add under `test/integration/temporal/testdata/`:
+- `compozy-standalone-memory.yaml` - Memory mode configuration
+- `compozy-standalone-file.yaml` - File-based persistence configuration
+- `compozy-standalone-no-ui.yaml` - UI disabled configuration
+- `compozy-standalone-custom-ports.yaml` - Custom ports
+- `simple-workflow.yaml` - Minimal workflow for testing
+- `multi-task-workflow.yaml` - Complex workflow for end-to-end testing
+
+## Mocks & Stubs
+
+- **No mocks for Temporal server** - Use real embedded server in integration tests
+- **May mock:** External services called by workflows/activities
+- **Prefer:** Real implementations over mocks whenever possible
+
+## Performance & Limits
+
+- **Startup Time:** Embedded server must start in <30 seconds (default timeout)
+- **Shutdown Time:** Graceful shutdown must complete in <10 seconds
+- **Memory Usage:** Baseline memory usage should be documented
+- **Concurrent Workflows:** Test with 10+ concurrent workflows to verify stability
+- **File Size:** Monitor SQLite file size growth (warn if >100MB in tests)
+
+## Observability Assertions
+
+- **Metrics Presence:**
+ - `compozy_temporal_standalone_enabled` gauge = 1 when standalone mode active
+ - `compozy_temporal_standalone_starts_total` counter increments on start
+ - `compozy_temporal_standalone_stops_total` counter increments on stop
+ - `compozy_temporal_standalone_errors_total` counter increments on errors
+
+- **Logs Presence:**
+ - "Starting embedded Temporal server" logged at Info level
+ - "Embedded Temporal server started successfully" logged at Info level
+ - "Standalone mode active" warning logged
+ - Error logs include context (phase, port, error details)
+
+- **Metrics Labels:**
+ - Mode: "memory" or "file"
+ - Error type: "start", "stop", "namespace"
+
+## CLI Tests (if applicable)
+
+- **Should show standalone mode in config output**
+ - `compozy config show -f table`
+ - Output includes: `temporal.mode = standalone`
+ - Output includes standalone configuration fields
+
+- **Should accept --temporal-mode flag**
+ - `compozy start --temporal-mode=standalone`
+ - Overrides config file
+ - Server starts in standalone mode
+
+## Exit Criteria
+
+- [ ] All unit tests exist and pass locally
+- [ ] All integration tests exist and pass locally
+- [ ] Test coverage >80% for new code in `engine/worker/embedded/`
+- [ ] CI pipeline updated to run standalone mode tests
+- [ ] Tests complete in <2 minutes total
+- [ ] No flaky tests (run 10 times, all pass)
+- [ ] Memory leaks checked (no goroutine leaks)
+- [ ] Performance benchmarks documented
+- [ ] Error scenarios covered with clear assertions
+- [ ] Web UI accessibility tested
+- [ ] Persistence across restarts verified
+- [ ] Port conflicts handled gracefully
+
+## Test Execution Commands
+
+```bash
+# Run all tests
+make test
+
+# Run only standalone mode tests
+go test -v ./engine/worker/embedded/... -race
+go test -v ./test/integration/temporal/... -race
+
+# Run specific test
+go test -v ./test/integration/temporal -run TestStandaloneWorkflowExecution -race
+
+# Run with coverage
+go test -v ./engine/worker/embedded/... -coverprofile=coverage.out
+go tool cover -html=coverage.out
+
+# Check for race conditions
+go test -v ./... -race -parallel=4
+
+# Benchmark
+go test -bench=. ./engine/worker/embedded/...
+```
+
+## CI Integration
+
+- Add `test-standalone` job to CI pipeline
+- Run standalone mode integration tests
+- Verify no Docker required for these tests
+- Check for goroutine leaks
+- Verify memory usage stays within bounds
+- Ensure tests pass on macOS, Linux, Windows
+
+## Notes
+
+- Tests should run quickly (<2 minutes total)
+- Use `t.Context()` instead of `context.Background()`
+- Clean up resources (database files, ports) after tests
+- Use table-driven tests for configuration validation
+- Document expected behavior in test names
+- Reference official Temporal tests when applicable
+
+## Related Planning Artifacts
+- tasks/prd-temporal/_techspec.md
+- tasks/prd-temporal/_docs.md
+- tasks/prd-temporal/_examples.md
+
+## References
+- Test Standards: `.cursor/rules/test-standards.mdc`
+- GitHub Reference: https://github.com/abtinf/temporal-a-day/blob/main/001-all-in-one-hello/main.go
+- Temporal Testing Docs: https://docs.temporal.io/develop/go/testing-suite
diff --git a/test/integration/temporal/errors_test.go b/test/integration/temporal/errors_test.go
new file mode 100644
index 00000000..1bc1ad21
--- /dev/null
+++ b/test/integration/temporal/errors_test.go
@@ -0,0 +1,119 @@
+package temporal
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/engine/worker/embedded"
+ "github.com/compozy/compozy/test/helpers"
+)
+
+func TestPortConflict(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ ctx := helpers.NewTestContext(t)
+ frontendPort := findAvailablePortRange(ctx, t, 4)
+ primaryCfg := newEmbeddedConfigFromDefaults()
+ primaryCfg.EnableUI = false
+ primaryCfg.FrontendPort = frontendPort
+ server := startStandaloneServer(ctx, t, primaryCfg)
+ t.Cleanup(func() {
+ stopTemporalServer(ctx, t, server)
+ })
+
+ conflictCtx := helpers.NewTestContext(t)
+ conflictCfg := newEmbeddedConfigFromDefaults()
+ conflictCfg.EnableUI = false
+ conflictCfg.FrontendPort = frontendPort
+
+ _, err := embedded.NewServer(conflictCtx, conflictCfg)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "already in use")
+ require.ErrorContains(t, err, fmt.Sprintf("%d", frontendPort))
+ require.ErrorContains(t, err, "adjust configuration")
+}
+
+func TestStartupTimeout(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ cfg.StartTimeout = time.Nanosecond
+
+ server, err := embedded.NewServer(ctx, cfg)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ stopTemporalServer(ctx, t, server)
+ })
+
+ startErr := server.Start(ctx)
+ require.Error(t, startErr)
+ require.ErrorContains(t, startErr, "wait for ready")
+ require.ErrorContains(t, startErr, "context deadline exceeded")
+}
+
+func TestInvalidDatabasePath(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+
+ tempFile := filepath.Join(t.TempDir(), "existing.file")
+ require.NoError(t, os.WriteFile(tempFile, []byte("placeholder"), 0o600))
+ cfg.DatabaseFile = filepath.Join(tempFile, "temporal.db")
+
+ _, err := embedded.NewServer(ctx, cfg)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "is not a directory")
+}
+
+func TestMissingDatabaseDirectory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ cfg.DatabaseFile = filepath.Join(t.TempDir(), "missing", "temporal.db")
+
+ _, err := embedded.NewServer(ctx, cfg)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "not accessible")
+}
+
+func TestDatabaseCorruption(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ ctx := helpers.NewTestContext(t)
+ dbDir := t.TempDir()
+ dbPath := filepath.Join(dbDir, "corrupt.db")
+ require.NoError(t, os.WriteFile(dbPath, []byte("not-a-sqlite-database"), 0o600))
+
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.DatabaseFile = dbPath
+
+ _, err := embedded.NewServer(ctx, cfg)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "create namespace")
+ require.ErrorContains(t, err, "not a database")
+}
diff --git a/test/integration/temporal/mode_switching_test.go b/test/integration/temporal/mode_switching_test.go
new file mode 100644
index 00000000..6427cf1d
--- /dev/null
+++ b/test/integration/temporal/mode_switching_test.go
@@ -0,0 +1,67 @@
+package temporal
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/engine/worker/embedded"
+ "github.com/compozy/compozy/pkg/config"
+ "github.com/compozy/compozy/test/helpers"
+)
+
+func TestDefaultModeIsRemote(t *testing.T) {
+ cfg := config.Default()
+ require.Equal(t, "remote", cfg.Temporal.Mode)
+ require.NotEmpty(t, cfg.Temporal.HostPort)
+}
+
+func TestStandaloneModeActivation(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := config.FromContext(ctx)
+
+ t.Run("Should activate standalone mode and run workflow", func(t *testing.T) {
+ oldHostPort := "remote.example:7233"
+ cfg.Temporal.HostPort = oldHostPort
+ cfg.Temporal.Mode = "standalone"
+ cfg.Temporal.Namespace = defaultNamespace()
+ cfg.Temporal.Standalone.DatabaseFile = ":memory:"
+ cfg.Temporal.Standalone.EnableUI = false
+ cfg.Temporal.Standalone.Namespace = cfg.Temporal.Namespace
+ cfg.Temporal.Standalone.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ embeddedCfg := toEmbeddedConfig(&cfg.Temporal.Standalone)
+ server := startStandaloneServer(ctx, t, embeddedCfg)
+ t.Cleanup(func() {
+ stopTemporalServer(ctx, t, server)
+ })
+ cfg.Temporal.HostPort = server.FrontendAddress()
+ require.NotEqual(t, oldHostPort, cfg.Temporal.HostPort)
+
+ exec := executeTestWorkflow(ctx, t, cfg.Temporal.HostPort, cfg.Temporal.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ })
+}
+
+func toEmbeddedConfig(cfg *config.StandaloneConfig) *embedded.Config {
+ if cfg == nil {
+ return newEmbeddedConfigFromDefaults()
+ }
+ return &embedded.Config{
+ DatabaseFile: cfg.DatabaseFile,
+ FrontendPort: cfg.FrontendPort,
+ BindIP: cfg.BindIP,
+ Namespace: cfg.Namespace,
+ ClusterName: cfg.ClusterName,
+ EnableUI: cfg.EnableUI,
+ RequireUI: cfg.RequireUI,
+ UIPort: cfg.UIPort,
+ LogLevel: cfg.LogLevel,
+ StartTimeout: cfg.StartTimeout,
+ }
+}
diff --git a/test/integration/temporal/persistence_test.go b/test/integration/temporal/persistence_test.go
new file mode 100644
index 00000000..1b64b899
--- /dev/null
+++ b/test/integration/temporal/persistence_test.go
@@ -0,0 +1,54 @@
+package temporal
+
+import (
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/test/helpers"
+ enumspb "go.temporal.io/api/enums/v1"
+)
+
+func TestStandalonePersistence(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should persist workflows across restarts", func(t *testing.T) {
+ ctx := helpers.NewTestContext(t)
+ dbPath := filepath.Join(t.TempDir(), "temporal.db")
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.DatabaseFile = dbPath
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ workflowID := "persistent-workflow"
+ firstRun, err := runWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace, workflowID)
+ require.NoError(t, err)
+ require.Equal(t, strings.ToUpper(firstRun.Input), firstRun.Result)
+ stopTemporalServer(ctx, t, server)
+
+ restartCtx := helpers.NewTestContext(t)
+ restartCfg := newEmbeddedConfigFromDefaults()
+ restartCfg.DatabaseFile = dbPath
+ restartCfg.EnableUI = false
+ restartCfg.FrontendPort = findAvailablePortRange(restartCtx, t, 4)
+ restartCfg.Namespace = cfg.Namespace
+ restarted := startStandaloneServer(restartCtx, t, restartCfg)
+ t.Cleanup(func() {
+ stopTemporalServer(restartCtx, t, restarted)
+ })
+ resp, err := describeWorkflow(
+ restartCtx,
+ t,
+ restarted.FrontendAddress(),
+ restartCfg.Namespace,
+ firstRun.WorkflowID,
+ firstRun.RunID,
+ )
+ require.NoError(t, err)
+ require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, resp.WorkflowExecutionInfo.Status)
+ })
+}
diff --git a/test/integration/temporal/standalone_test.go b/test/integration/temporal/standalone_test.go
new file mode 100644
index 00000000..1afaa028
--- /dev/null
+++ b/test/integration/temporal/standalone_test.go
@@ -0,0 +1,319 @@
+package temporal
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/engine/worker/embedded"
+ "github.com/compozy/compozy/pkg/config"
+ "github.com/compozy/compozy/test/helpers"
+ enumspb "go.temporal.io/api/enums/v1"
+ workflowservice "go.temporal.io/api/workflowservice/v1"
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/worker"
+ "go.temporal.io/sdk/workflow"
+)
+
+const (
+ testTaskQueue = "temporal-standalone-integration"
+ workflowTimeout = 30 * time.Second
+)
+
+type workflowExecution struct {
+ WorkflowID string
+ RunID string
+ Input string
+ Result string
+}
+
+type workflowInput struct {
+ Name string `json:"name"`
+}
+
+func TestStandaloneMemoryMode(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should execute workflow using in-memory persistence", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.DatabaseFile = ":memory:"
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ exec := executeTestWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ })
+}
+
+func TestStandaloneFileMode(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should persist workflow results to disk", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ dbPath := filepath.Join(t.TempDir(), "temporal.db")
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.DatabaseFile = dbPath
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ exec := executeTestWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ require.Eventually(t, func() bool {
+ info, err := os.Stat(dbPath)
+ return err == nil && info.Size() > 0
+ }, 5*time.Second, 100*time.Millisecond)
+ })
+}
+
+func TestStandaloneCustomPorts(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should honor custom port selection", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ frontendPort := findAvailablePortRange(ctx, t, 4)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.DatabaseFile = ":memory:"
+ cfg.FrontendPort = frontendPort
+ cfg.EnableUI = false
+ server := startStandaloneServer(ctx, t, cfg)
+ require.Equal(t, fmt.Sprintf("%s:%d", cfg.BindIP, cfg.FrontendPort), server.FrontendAddress())
+ exec := executeTestWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ })
+}
+
+func TestStandaloneWorkflowExecution(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should report completed workflow execution", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.DatabaseFile = ":memory:"
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ exec := executeTestWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ desc, err := describeWorkflow(ctx, t, server.FrontendAddress(), cfg.Namespace, exec.WorkflowID, exec.RunID)
+ require.NoError(t, err)
+ require.Equal(t, enumspb.WORKFLOW_EXECUTION_STATUS_COMPLETED, desc.WorkflowExecutionInfo.Status)
+ })
+}
+
+func startStandaloneServer(ctx context.Context, t *testing.T, cfg *embedded.Config) *embedded.Server {
+ t.Helper()
+ server, err := embedded.NewServer(ctx, cfg)
+ require.NoError(t, err)
+ require.NoError(t, server.Start(ctx))
+ t.Cleanup(func() {
+ stopTemporalServer(ctx, t, server)
+ })
+ return server
+}
+
+func stopTemporalServer(ctx context.Context, t *testing.T, server *embedded.Server) {
+ t.Helper()
+ stopCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second)
+ defer cancel()
+ require.NoError(t, server.Stop(stopCtx))
+}
+
+func executeTestWorkflow(
+ ctx context.Context,
+ t *testing.T,
+ address string,
+ namespace string,
+ workflowID ...string,
+) workflowExecution {
+ t.Helper()
+ id := ""
+ if len(workflowID) > 0 {
+ id = workflowID[0]
+ }
+ if id == "" {
+ id = fmt.Sprintf("standalone-%s", uuid.NewString())
+ }
+ exec, err := runWorkflow(ctx, t, address, namespace, id)
+ require.NoError(t, err)
+ return exec
+}
+
+func runWorkflow(
+ ctx context.Context,
+ t *testing.T,
+ address string,
+ namespace string,
+ workflowID string,
+) (workflowExecution, error) {
+ t.Helper()
+ c := dialTemporalClient(t, address, namespace)
+ defer closeTemporalClient(t, c)
+ w := startTestWorker(t, c)
+ defer stopWorker(w)
+ input := loadWorkflowInput(t)
+ runCtx, cancel := context.WithTimeout(ctx, workflowTimeout)
+ defer cancel()
+ opts := client.StartWorkflowOptions{
+ ID: workflowID,
+ TaskQueue: testTaskQueue,
+ WorkflowIDReusePolicy: enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE,
+ }
+ if opts.WorkflowIDReusePolicy == enumspb.WORKFLOW_ID_REUSE_POLICY_UNSPECIFIED {
+ opts.WorkflowIDReusePolicy = enumspb.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE
+ }
+ run, err := c.ExecuteWorkflow(runCtx, opts, integrationWorkflow, input.Name)
+ if err != nil {
+ return workflowExecution{}, err
+ }
+ var result string
+ if err := run.Get(runCtx, &result); err != nil {
+ return workflowExecution{}, err
+ }
+ return workflowExecution{WorkflowID: opts.ID, RunID: run.GetRunID(), Input: input.Name, Result: result}, nil
+}
+
+func describeWorkflow(
+ ctx context.Context,
+ t *testing.T,
+ address string,
+ namespace string,
+ workflowID string,
+ runID string,
+) (*workflowservice.DescribeWorkflowExecutionResponse, error) {
+ t.Helper()
+ client := dialTemporalClient(t, address, namespace)
+ defer closeTemporalClient(t, client)
+ describeCtx, cancel := context.WithTimeout(ctx, workflowTimeout)
+ defer cancel()
+ return client.DescribeWorkflowExecution(describeCtx, workflowID, runID)
+}
+
+func dialTemporalClient(t *testing.T, address string, namespace string) client.Client {
+ t.Helper()
+ c, err := client.Dial(client.Options{HostPort: address, Namespace: namespace})
+ require.NoError(t, err)
+ return c
+}
+
+func closeTemporalClient(t *testing.T, c client.Client) {
+ t.Helper()
+ c.Close()
+}
+
+func startTestWorker(t *testing.T, c client.Client) worker.Worker {
+ t.Helper()
+ w := worker.New(c, testTaskQueue, worker.Options{})
+ w.RegisterWorkflow(integrationWorkflow)
+ w.RegisterActivity(integrationActivity)
+ require.NoError(t, w.Start())
+ return w
+}
+
+func stopWorker(w worker.Worker) {
+ w.Stop()
+}
+
+func integrationWorkflow(ctx workflow.Context, name string) (string, error) {
+ options := workflow.ActivityOptions{StartToCloseTimeout: 10 * time.Second}
+ ctx = workflow.WithActivityOptions(ctx, options)
+ var result string
+ if err := workflow.ExecuteActivity(ctx, integrationActivity, name).Get(ctx, &result); err != nil {
+ return "", err
+ }
+ return result, nil
+}
+
+func integrationActivity(_ context.Context, name string) (string, error) {
+ if strings.TrimSpace(name) == "" {
+ return "", fmt.Errorf("name is required")
+ }
+ return strings.ToUpper(name), nil
+}
+
+func loadWorkflowInput(t *testing.T) workflowInput {
+ t.Helper()
+ path := filepath.Join("testdata", "workflow_input.json")
+ data, err := os.ReadFile(path)
+ require.NoError(t, err)
+ var input workflowInput
+ require.NoError(t, json.Unmarshal(data, &input))
+ return input
+}
+
+func findAvailablePortRange(ctx context.Context, t *testing.T, size int) int {
+ t.Helper()
+ for port := 15000; port < 25000; port++ {
+ if !portsAvailable(ctx, port, size) {
+ continue
+ }
+ // Ensure auxiliary port at +1000 offset is available for Temporal UI when enabled
+ if !portAvailable(ctx, port+1000) {
+ continue
+ }
+ return port
+ }
+ t.Fatalf("no available port range found")
+ return 0
+}
+
+func portsAvailable(ctx context.Context, start int, size int) bool {
+ for offset := 0; offset < size; offset++ {
+ if !portAvailable(ctx, start+offset) {
+ return false
+ }
+ }
+ return true
+}
+
+func portAvailable(ctx context.Context, port int) bool {
+ dialCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
+ ln, err := (&net.ListenConfig{}).Listen(dialCtx, "tcp", fmt.Sprintf("127.0.0.1:%d", port))
+ cancel()
+ if err != nil {
+ return false
+ }
+ _ = ln.Close()
+ return true
+}
+
+func newEmbeddedConfigFromDefaults() *embedded.Config {
+ defaults := config.Default().Temporal.Standalone
+ return &embedded.Config{
+ DatabaseFile: defaults.DatabaseFile,
+ FrontendPort: defaults.FrontendPort,
+ BindIP: defaults.BindIP,
+ Namespace: defaults.Namespace,
+ ClusterName: defaults.ClusterName,
+ EnableUI: defaults.EnableUI,
+ RequireUI: defaults.RequireUI,
+ UIPort: defaults.UIPort,
+ LogLevel: defaults.LogLevel,
+ StartTimeout: defaults.StartTimeout,
+ }
+}
+
+func defaultNamespace() string {
+ return config.Default().Temporal.Namespace
+}
diff --git a/test/integration/temporal/startup_lifecycle_test.go b/test/integration/temporal/startup_lifecycle_test.go
new file mode 100644
index 00000000..4855ee6b
--- /dev/null
+++ b/test/integration/temporal/startup_lifecycle_test.go
@@ -0,0 +1,221 @@
+package temporal
+
+import (
+ "context"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+
+ "github.com/compozy/compozy/engine/worker/embedded"
+ "github.com/compozy/compozy/test/helpers"
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/worker"
+ "go.temporal.io/sdk/workflow"
+)
+
+const lifecycleTaskQueue = "temporal-startup-lifecycle"
+
+func TestGracefulShutdownDuringStartup(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should cancel startup gracefully", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server, err := embedded.NewServer(ctx, cfg)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ stopTemporalServer(ctx, t, server)
+ })
+ startCtx, cancel := context.WithCancel(ctx)
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- server.Start(startCtx)
+ }()
+ cancel()
+ startErr := <-errCh
+ require.Error(t, startErr)
+ require.ErrorContains(t, startErr, "context canceled")
+ require.ErrorContains(t, startErr, "wait for ready")
+ stopCtx, stopCancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
+ defer stopCancel()
+ require.NoError(t, server.Stop(stopCtx))
+ })
+}
+
+func TestMultipleStartCalls(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should fail when starting an already running server", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ err := server.Start(ctx)
+ require.Error(t, err)
+ require.ErrorContains(t, err, "already started")
+ })
+}
+
+func TestMultipleStopCalls(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should allow repeated stop calls", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ firstStopCtx, firstCancel := context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second)
+ require.NoError(t, server.Stop(firstStopCtx))
+ firstCancel()
+ secondStopCtx, secondCancel := context.WithTimeout(context.WithoutCancel(ctx), 20*time.Second)
+ require.NoError(t, server.Stop(secondStopCtx))
+ secondCancel()
+ })
+}
+
+func TestConcurrentRequests(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should coordinate shutdown during concurrent workflow execution", func(t *testing.T) {
+ t.Helper()
+ ctx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.FrontendPort = findAvailablePortRange(ctx, t, 4)
+ server := startStandaloneServer(ctx, t, cfg)
+ lifecycleClient := dialTemporalClient(t, server.FrontendAddress(), cfg.Namespace)
+ defer closeTemporalClient(t, lifecycleClient)
+ lifecycleWorker := worker.New(lifecycleClient, lifecycleTaskQueue, worker.Options{})
+ lifecycleWorker.RegisterWorkflow(lifecycleWorkflow)
+ lifecycleWorker.RegisterActivity(lifecycleActivity)
+ require.NoError(t, lifecycleWorker.Start())
+ defer lifecycleWorker.Stop()
+ const workflowCount = 12
+ results := make([]error, 0, workflowCount)
+ var resultsMu sync.Mutex
+ var wg sync.WaitGroup
+ for i := 0; i < workflowCount; i++ {
+ idx := i
+ wg.Go(func() {
+ err := runLifecycleWorkflow(ctx, lifecycleClient, idx)
+ resultsMu.Lock()
+ results = append(results, err)
+ resultsMu.Unlock()
+ })
+ }
+ time.Sleep(3 * time.Second)
+ stopCtx, stopCancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
+ defer stopCancel()
+ stopErrCh := make(chan error, 1)
+ go func() {
+ stopErrCh <- server.Stop(stopCtx)
+ }()
+ wg.Wait()
+ require.NoError(t, <-stopErrCh)
+ successes := 0
+ cancellations := 0
+ for _, err := range results {
+ if err == nil {
+ successes++
+ continue
+ }
+ cancellations++
+ errMsg := err.Error()
+ require.Truef(
+ t,
+ strings.Contains(errMsg, "context canceled") ||
+ strings.Contains(errMsg, "context deadline exceeded") ||
+ strings.Contains(errMsg, "transport is closing"),
+ "unexpected workflow error: %v",
+ err,
+ )
+ }
+ require.Greater(t, successes, 0)
+ require.Equal(t, workflowCount, successes+cancellations)
+ })
+}
+
+func TestServerRestartCycle(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping temporal integration tests in short mode")
+ }
+
+ t.Run("Should restart standalone server without data loss", func(t *testing.T) {
+ t.Helper()
+ dbPath := filepath.Join(t.TempDir(), "temporal-restart.db")
+ for i := 0; i < 2; i++ {
+ cycleCtx := helpers.NewTestContext(t)
+ cfg := newEmbeddedConfigFromDefaults()
+ cfg.EnableUI = false
+ cfg.DatabaseFile = dbPath
+ cfg.FrontendPort = findAvailablePortRange(cycleCtx, t, 4)
+ server := startStandaloneServer(cycleCtx, t, cfg)
+ exec := executeTestWorkflow(cycleCtx, t, server.FrontendAddress(), cfg.Namespace)
+ require.Equal(t, strings.ToUpper(exec.Input), exec.Result)
+ stopTemporalServer(cycleCtx, t, server)
+ }
+ })
+}
+
+func runLifecycleWorkflow(ctx context.Context, c client.Client, idx int) error {
+ runCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+ opts := client.StartWorkflowOptions{
+ ID: fmt.Sprintf("lifecycle-%d-%s", idx, uuid.NewString()),
+ TaskQueue: lifecycleTaskQueue,
+ }
+ run, err := c.ExecuteWorkflow(runCtx, opts, lifecycleWorkflow, fmt.Sprintf("workflow-%d", idx))
+ if err != nil {
+ return err
+ }
+ var result string
+ if err := run.Get(runCtx, &result); err != nil {
+ return err
+ }
+ if result == "" {
+ return fmt.Errorf("empty result")
+ }
+ return nil
+}
+
+// lifecycleWorkflow executes a slower workflow to exercise shutdown behavior.
+func lifecycleWorkflow(ctx workflow.Context, name string) (string, error) {
+ options := workflow.ActivityOptions{StartToCloseTimeout: 5 * time.Second}
+ ctx = workflow.WithActivityOptions(ctx, options)
+ var result string
+ if err := workflow.ExecuteActivity(ctx, lifecycleActivity, name).Get(ctx, &result); err != nil {
+ return "", err
+ }
+ return result, nil
+}
+
+// lifecycleActivity simulates a heavier activity load before delegating to the default integration activity.
+func lifecycleActivity(ctx context.Context, name string) (string, error) {
+ select {
+ case <-time.After(250 * time.Millisecond):
+ case <-ctx.Done():
+ return "", ctx.Err()
+ }
+ return integrationActivity(ctx, name)
+}
diff --git a/test/integration/temporal/testdata/workflow_input.json b/test/integration/temporal/testdata/workflow_input.json
new file mode 100644
index 00000000..71c8a471
--- /dev/null
+++ b/test/integration/temporal/testdata/workflow_input.json
@@ -0,0 +1,3 @@
+{
+ "name": "Temporal Integration"
+}