forge-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/worktree-create.sh +64 -0
- package/.claude/hooks/worktree-remove.sh +57 -0
- package/.claude/settings.local.json +29 -0
- package/.forge/knowledge/conventions.yaml +1 -0
- package/.forge/knowledge/decisions.yaml +1 -0
- package/.forge/knowledge/gotchas.yaml +1 -0
- package/.forge/knowledge/patterns.yaml +1 -0
- package/.forge/manifest.yaml +6 -0
- package/CLAUDE.md +144 -0
- package/bin/setup-forge.sh +132 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +553 -0
- package/dist/cli.js.map +1 -0
- package/dist/context/codebase.d.ts +57 -0
- package/dist/context/codebase.d.ts.map +1 -0
- package/dist/context/codebase.js +301 -0
- package/dist/context/codebase.js.map +1 -0
- package/dist/context/injector.d.ts +147 -0
- package/dist/context/injector.d.ts.map +1 -0
- package/dist/context/injector.js +533 -0
- package/dist/context/injector.js.map +1 -0
- package/dist/context/memory.d.ts +32 -0
- package/dist/context/memory.d.ts.map +1 -0
- package/dist/context/memory.js +140 -0
- package/dist/context/memory.js.map +1 -0
- package/dist/context/session-index.d.ts +54 -0
- package/dist/context/session-index.d.ts.map +1 -0
- package/dist/context/session-index.js +265 -0
- package/dist/context/session-index.js.map +1 -0
- package/dist/context/session.d.ts +42 -0
- package/dist/context/session.d.ts.map +1 -0
- package/dist/context/session.js +121 -0
- package/dist/context/session.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/chunker.d.ts +19 -0
- package/dist/ingestion/chunker.d.ts.map +1 -0
- package/dist/ingestion/chunker.js +189 -0
- package/dist/ingestion/chunker.js.map +1 -0
- package/dist/ingestion/embedder.d.ts +45 -0
- package/dist/ingestion/embedder.d.ts.map +1 -0
- package/dist/ingestion/embedder.js +152 -0
- package/dist/ingestion/embedder.js.map +1 -0
- package/dist/ingestion/git-analyzer.d.ts +77 -0
- package/dist/ingestion/git-analyzer.d.ts.map +1 -0
- package/dist/ingestion/git-analyzer.js +437 -0
- package/dist/ingestion/git-analyzer.js.map +1 -0
- package/dist/ingestion/indexer.d.ts +79 -0
- package/dist/ingestion/indexer.d.ts.map +1 -0
- package/dist/ingestion/indexer.js +766 -0
- package/dist/ingestion/indexer.js.map +1 -0
- package/dist/ingestion/markdown-chunker.d.ts +19 -0
- package/dist/ingestion/markdown-chunker.d.ts.map +1 -0
- package/dist/ingestion/markdown-chunker.js +243 -0
- package/dist/ingestion/markdown-chunker.js.map +1 -0
- package/dist/ingestion/markdown-knowledge.d.ts +21 -0
- package/dist/ingestion/markdown-knowledge.d.ts.map +1 -0
- package/dist/ingestion/markdown-knowledge.js +129 -0
- package/dist/ingestion/markdown-knowledge.js.map +1 -0
- package/dist/ingestion/parser.d.ts +20 -0
- package/dist/ingestion/parser.d.ts.map +1 -0
- package/dist/ingestion/parser.js +429 -0
- package/dist/ingestion/parser.js.map +1 -0
- package/dist/ingestion/watcher.d.ts +28 -0
- package/dist/ingestion/watcher.d.ts.map +1 -0
- package/dist/ingestion/watcher.js +147 -0
- package/dist/ingestion/watcher.js.map +1 -0
- package/dist/knowledge/hydrator.d.ts +37 -0
- package/dist/knowledge/hydrator.d.ts.map +1 -0
- package/dist/knowledge/hydrator.js +220 -0
- package/dist/knowledge/hydrator.js.map +1 -0
- package/dist/knowledge/registry.d.ts +129 -0
- package/dist/knowledge/registry.d.ts.map +1 -0
- package/dist/knowledge/registry.js +361 -0
- package/dist/knowledge/registry.js.map +1 -0
- package/dist/knowledge/search.d.ts +114 -0
- package/dist/knowledge/search.d.ts.map +1 -0
- package/dist/knowledge/search.js +428 -0
- package/dist/knowledge/search.js.map +1 -0
- package/dist/knowledge/store.d.ts +76 -0
- package/dist/knowledge/store.d.ts.map +1 -0
- package/dist/knowledge/store.js +230 -0
- package/dist/knowledge/store.js.map +1 -0
- package/dist/learning/confidence.d.ts +30 -0
- package/dist/learning/confidence.d.ts.map +1 -0
- package/dist/learning/confidence.js +165 -0
- package/dist/learning/confidence.js.map +1 -0
- package/dist/learning/patterns.d.ts +52 -0
- package/dist/learning/patterns.d.ts.map +1 -0
- package/dist/learning/patterns.js +290 -0
- package/dist/learning/patterns.js.map +1 -0
- package/dist/learning/trajectory.d.ts +55 -0
- package/dist/learning/trajectory.d.ts.map +1 -0
- package/dist/learning/trajectory.js +200 -0
- package/dist/learning/trajectory.js.map +1 -0
- package/dist/memory/memory-compat.d.ts +100 -0
- package/dist/memory/memory-compat.d.ts.map +1 -0
- package/dist/memory/memory-compat.js +146 -0
- package/dist/memory/memory-compat.js.map +1 -0
- package/dist/memory/observation-store.d.ts +57 -0
- package/dist/memory/observation-store.d.ts.map +1 -0
- package/dist/memory/observation-store.js +154 -0
- package/dist/memory/observation-store.js.map +1 -0
- package/dist/memory/session-tracker.d.ts +81 -0
- package/dist/memory/session-tracker.d.ts.map +1 -0
- package/dist/memory/session-tracker.js +262 -0
- package/dist/memory/session-tracker.js.map +1 -0
- package/dist/pipeline/engine.d.ts +179 -0
- package/dist/pipeline/engine.d.ts.map +1 -0
- package/dist/pipeline/engine.js +691 -0
- package/dist/pipeline/engine.js.map +1 -0
- package/dist/pipeline/events.d.ts +54 -0
- package/dist/pipeline/events.d.ts.map +1 -0
- package/dist/pipeline/events.js +157 -0
- package/dist/pipeline/events.js.map +1 -0
- package/dist/pipeline/parallel.d.ts +83 -0
- package/dist/pipeline/parallel.d.ts.map +1 -0
- package/dist/pipeline/parallel.js +277 -0
- package/dist/pipeline/parallel.js.map +1 -0
- package/dist/pipeline/state-machine.d.ts +65 -0
- package/dist/pipeline/state-machine.d.ts.map +1 -0
- package/dist/pipeline/state-machine.js +176 -0
- package/dist/pipeline/state-machine.js.map +1 -0
- package/dist/query/graph-queries.d.ts +84 -0
- package/dist/query/graph-queries.d.ts.map +1 -0
- package/dist/query/graph-queries.js +216 -0
- package/dist/query/graph-queries.js.map +1 -0
- package/dist/query/hybrid-search.d.ts +34 -0
- package/dist/query/hybrid-search.d.ts.map +1 -0
- package/dist/query/hybrid-search.js +263 -0
- package/dist/query/hybrid-search.js.map +1 -0
- package/dist/query/intent-detector.d.ts +35 -0
- package/dist/query/intent-detector.d.ts.map +1 -0
- package/dist/query/intent-detector.js +115 -0
- package/dist/query/intent-detector.js.map +1 -0
- package/dist/query/ranking.d.ts +57 -0
- package/dist/query/ranking.d.ts.map +1 -0
- package/dist/query/ranking.js +109 -0
- package/dist/query/ranking.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +291 -0
- package/dist/server.js.map +1 -0
- package/dist/storage/falkordb-store.d.ts +73 -0
- package/dist/storage/falkordb-store.d.ts.map +1 -0
- package/dist/storage/falkordb-store.js +346 -0
- package/dist/storage/falkordb-store.js.map +1 -0
- package/dist/storage/file-cache.d.ts +32 -0
- package/dist/storage/file-cache.d.ts.map +1 -0
- package/dist/storage/file-cache.js +115 -0
- package/dist/storage/file-cache.js.map +1 -0
- package/dist/storage/interfaces.d.ts +151 -0
- package/dist/storage/interfaces.d.ts.map +1 -0
- package/dist/storage/interfaces.js +7 -0
- package/dist/storage/interfaces.js.map +1 -0
- package/dist/storage/qdrant-store.d.ts +110 -0
- package/dist/storage/qdrant-store.d.ts.map +1 -0
- package/dist/storage/qdrant-store.js +467 -0
- package/dist/storage/qdrant-store.js.map +1 -0
- package/dist/storage/schema.d.ts +4 -0
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/schema.js +136 -0
- package/dist/storage/schema.js.map +1 -0
- package/dist/storage/sqlite.d.ts +35 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +132 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/tools/collaboration-tools.d.ts +111 -0
- package/dist/tools/collaboration-tools.d.ts.map +1 -0
- package/dist/tools/collaboration-tools.js +174 -0
- package/dist/tools/collaboration-tools.js.map +1 -0
- package/dist/tools/context-tools.d.ts +293 -0
- package/dist/tools/context-tools.d.ts.map +1 -0
- package/dist/tools/context-tools.js +437 -0
- package/dist/tools/context-tools.js.map +1 -0
- package/dist/tools/graph-tools.d.ts +129 -0
- package/dist/tools/graph-tools.d.ts.map +1 -0
- package/dist/tools/graph-tools.js +237 -0
- package/dist/tools/graph-tools.js.map +1 -0
- package/dist/tools/ingestion-tools.d.ts +96 -0
- package/dist/tools/ingestion-tools.d.ts.map +1 -0
- package/dist/tools/ingestion-tools.js +90 -0
- package/dist/tools/ingestion-tools.js.map +1 -0
- package/dist/tools/learning-tools.d.ts +168 -0
- package/dist/tools/learning-tools.d.ts.map +1 -0
- package/dist/tools/learning-tools.js +158 -0
- package/dist/tools/learning-tools.js.map +1 -0
- package/dist/tools/memory-tools.d.ts +183 -0
- package/dist/tools/memory-tools.d.ts.map +1 -0
- package/dist/tools/memory-tools.js +197 -0
- package/dist/tools/memory-tools.js.map +1 -0
- package/dist/tools/phase-tools.d.ts +954 -0
- package/dist/tools/phase-tools.d.ts.map +1 -0
- package/dist/tools/phase-tools.js +1215 -0
- package/dist/tools/phase-tools.js.map +1 -0
- package/dist/tools/pipeline-tools.d.ts +140 -0
- package/dist/tools/pipeline-tools.d.ts.map +1 -0
- package/dist/tools/pipeline-tools.js +162 -0
- package/dist/tools/pipeline-tools.js.map +1 -0
- package/dist/tools/registration-tools.d.ts +220 -0
- package/dist/tools/registration-tools.d.ts.map +1 -0
- package/dist/tools/registration-tools.js +391 -0
- package/dist/tools/registration-tools.js.map +1 -0
- package/dist/util/circuit-breaker.d.ts +75 -0
- package/dist/util/circuit-breaker.d.ts.map +1 -0
- package/dist/util/circuit-breaker.js +159 -0
- package/dist/util/circuit-breaker.js.map +1 -0
- package/dist/util/config.d.ts +23 -0
- package/dist/util/config.d.ts.map +1 -0
- package/dist/util/config.js +164 -0
- package/dist/util/config.js.map +1 -0
- package/dist/util/logger.d.ts +13 -0
- package/dist/util/logger.d.ts.map +1 -0
- package/dist/util/logger.js +45 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/token-counter.d.ts +24 -0
- package/dist/util/token-counter.d.ts.map +1 -0
- package/dist/util/token-counter.js +48 -0
- package/dist/util/token-counter.js.map +1 -0
- package/dist/util/types.d.ts +525 -0
- package/dist/util/types.d.ts.map +1 -0
- package/dist/util/types.js +5 -0
- package/dist/util/types.js.map +1 -0
- package/docker-compose.yml +20 -0
- package/docs/plans/2026-02-27-swarm-coordination/architecture.md +203 -0
- package/docs/plans/2026-02-27-swarm-coordination/vision.md +57 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/architecture.md +1 -0
- package/docs/plans/completed/2026-02-26-forge-plugin-bundling/vision.md +300 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/architecture.md +480 -0
- package/docs/plans/completed/2026-02-27-forge-swarm-learning/verification-checklist.md +462 -0
- package/docs/plans/completed/2026-02-27-git-history-atlassian/git-jira-plan.md +181 -0
- package/package.json +39 -0
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/.mcp.json +15 -0
- package/plugin/README.md +134 -0
- package/plugin/agents/architect.md +367 -0
- package/plugin/agents/backend-specialist.md +263 -0
- package/plugin/agents/brainstormer.md +122 -0
- package/plugin/agents/data-specialist.md +266 -0
- package/plugin/agents/designer.md +408 -0
- package/plugin/agents/frontend-specialist.md +241 -0
- package/plugin/agents/inspector.md +406 -0
- package/plugin/agents/knowledge-keeper.md +443 -0
- package/plugin/agents/platform-engineer.md +326 -0
- package/plugin/agents/product-manager.md +268 -0
- package/plugin/agents/product-owner.md +438 -0
- package/plugin/agents/pulse-checker.md +73 -0
- package/plugin/agents/qa-strategist.md +500 -0
- package/plugin/agents/self-improver.md +310 -0
- package/plugin/agents/strategist.md +360 -0
- package/plugin/agents/supervisor.md +380 -0
- package/plugin/commands/brainstorm.md +25 -0
- package/plugin/commands/forge.md +88 -0
- package/plugin/docs/atlassian-integration.md +110 -0
- package/plugin/docs/workflow.md +126 -0
- package/plugin/skills/agent-development/.skillfish.json +10 -0
- package/plugin/skills/agent-development/SKILL.md +415 -0
- package/plugin/skills/agent-development/examples/agent-creation-prompt.md +238 -0
- package/plugin/skills/agent-development/examples/complete-agent-examples.md +427 -0
- package/plugin/skills/agent-development/references/agent-creation-system-prompt.md +207 -0
- package/plugin/skills/agent-development/references/system-prompt-design.md +411 -0
- package/plugin/skills/agent-development/references/triggering-examples.md +491 -0
- package/plugin/skills/agent-development/scripts/validate-agent.sh +217 -0
- package/plugin/skills/agent-handoff/SKILL.md +335 -0
- package/plugin/skills/anti-stub/SKILL.md +317 -0
- package/plugin/skills/brainstorm/SKILL.md +31 -0
- package/plugin/skills/debugging/SKILL.md +276 -0
- package/plugin/skills/fix/SKILL.md +62 -0
- package/plugin/skills/frontend-design/.skillfish.json +10 -0
- package/plugin/skills/frontend-design/SKILL.md +42 -0
- package/plugin/skills/gotchas/SKILL.md +61 -0
- package/plugin/skills/graph-orchestrator/SKILL.md +38 -0
- package/plugin/skills/history/SKILL.md +58 -0
- package/plugin/skills/impact/SKILL.md +59 -0
- package/plugin/skills/implementation-execution/SKILL.md +291 -0
- package/plugin/skills/index-repo/SKILL.md +55 -0
- package/plugin/skills/interviewing/SKILL.md +225 -0
- package/plugin/skills/knowledge-curation/SKILL.md +393 -0
- package/plugin/skills/learn/SKILL.md +69 -0
- package/plugin/skills/mcp-integration/.skillfish.json +10 -0
- package/plugin/skills/mcp-integration/SKILL.md +554 -0
- package/plugin/skills/mcp-integration/examples/http-server.json +20 -0
- package/plugin/skills/mcp-integration/examples/sse-server.json +19 -0
- package/plugin/skills/mcp-integration/examples/stdio-server.json +26 -0
- package/plugin/skills/mcp-integration/references/authentication.md +549 -0
- package/plugin/skills/mcp-integration/references/server-types.md +536 -0
- package/plugin/skills/mcp-integration/references/tool-usage.md +538 -0
- package/plugin/skills/nestjs/.skillfish.json +10 -0
- package/plugin/skills/nestjs/SKILL.md +669 -0
- package/plugin/skills/nestjs/drizzle-reference.md +1894 -0
- package/plugin/skills/nestjs/reference.md +1447 -0
- package/plugin/skills/nestjs/workflow-optimization.md +229 -0
- package/plugin/skills/parallel-dispatch/SKILL.md +308 -0
- package/plugin/skills/project-discovery/SKILL.md +304 -0
- package/plugin/skills/search/SKILL.md +56 -0
- package/plugin/skills/security-audit/SKILL.md +362 -0
- package/plugin/skills/skill-development/.skillfish.json +10 -0
- package/plugin/skills/skill-development/SKILL.md +637 -0
- package/plugin/skills/skill-development/references/skill-creator-original.md +209 -0
- package/plugin/skills/tdd/SKILL.md +273 -0
- package/plugin/skills/terminal-presentation/SKILL.md +395 -0
- package/plugin/skills/test-strategy/SKILL.md +365 -0
- package/plugin/skills/verification-protocol/SKILL.md +256 -0
- package/plugin/skills/visual-explainer/CHANGELOG.md +97 -0
- package/plugin/skills/visual-explainer/LICENSE +21 -0
- package/plugin/skills/visual-explainer/README.md +137 -0
- package/plugin/skills/visual-explainer/SKILL.md +352 -0
- package/plugin/skills/visual-explainer/banner.png +0 -0
- package/plugin/skills/visual-explainer/package.json +11 -0
- package/plugin/skills/visual-explainer/prompts/diff-review.md +68 -0
- package/plugin/skills/visual-explainer/prompts/fact-check.md +63 -0
- package/plugin/skills/visual-explainer/prompts/generate-slides.md +18 -0
- package/plugin/skills/visual-explainer/prompts/generate-web-diagram.md +10 -0
- package/plugin/skills/visual-explainer/prompts/plan-review.md +86 -0
- package/plugin/skills/visual-explainer/prompts/project-recap.md +61 -0
- package/plugin/skills/visual-explainer/references/css-patterns.md +1188 -0
- package/plugin/skills/visual-explainer/references/libraries.md +470 -0
- package/plugin/skills/visual-explainer/references/responsive-nav.md +212 -0
- package/plugin/skills/visual-explainer/references/slide-patterns.md +1403 -0
- package/plugin/skills/visual-explainer/templates/architecture.html +596 -0
- package/plugin/skills/visual-explainer/templates/data-table.html +540 -0
- package/plugin/skills/visual-explainer/templates/mermaid-flowchart.html +435 -0
- package/plugin/skills/visual-explainer/templates/slide-deck.html +913 -0
- package/src/cli.ts +655 -0
- package/src/context/.gitkeep +0 -0
- package/src/context/codebase.ts +393 -0
- package/src/context/injector.ts +797 -0
- package/src/context/memory.ts +187 -0
- package/src/context/session-index.ts +327 -0
- package/src/context/session.ts +152 -0
- package/src/index.ts +47 -0
- package/src/ingestion/.gitkeep +0 -0
- package/src/ingestion/chunker.ts +277 -0
- package/src/ingestion/embedder.ts +167 -0
- package/src/ingestion/git-analyzer.ts +545 -0
- package/src/ingestion/indexer.ts +984 -0
- package/src/ingestion/markdown-chunker.ts +337 -0
- package/src/ingestion/markdown-knowledge.ts +175 -0
- package/src/ingestion/parser.ts +475 -0
- package/src/ingestion/watcher.ts +182 -0
- package/src/knowledge/.gitkeep +0 -0
- package/src/knowledge/hydrator.ts +246 -0
- package/src/knowledge/registry.ts +463 -0
- package/src/knowledge/search.ts +565 -0
- package/src/knowledge/store.ts +262 -0
- package/src/learning/.gitkeep +0 -0
- package/src/learning/confidence.ts +193 -0
- package/src/learning/patterns.ts +360 -0
- package/src/learning/trajectory.ts +268 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/memory-compat.ts +233 -0
- package/src/memory/observation-store.ts +224 -0
- package/src/memory/session-tracker.ts +332 -0
- package/src/pipeline/.gitkeep +0 -0
- package/src/pipeline/engine.ts +1139 -0
- package/src/pipeline/events.ts +253 -0
- package/src/pipeline/parallel.ts +394 -0
- package/src/pipeline/state-machine.ts +199 -0
- package/src/query/.gitkeep +0 -0
- package/src/query/graph-queries.ts +262 -0
- package/src/query/hybrid-search.ts +337 -0
- package/src/query/intent-detector.ts +131 -0
- package/src/query/ranking.ts +161 -0
- package/src/server.ts +352 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/falkordb-store.ts +388 -0
- package/src/storage/file-cache.ts +141 -0
- package/src/storage/interfaces.ts +201 -0
- package/src/storage/qdrant-store.ts +557 -0
- package/src/storage/schema.ts +139 -0
- package/src/storage/sqlite.ts +168 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/collaboration-tools.ts +208 -0
- package/src/tools/context-tools.ts +493 -0
- package/src/tools/graph-tools.ts +295 -0
- package/src/tools/ingestion-tools.ts +122 -0
- package/src/tools/learning-tools.ts +181 -0
- package/src/tools/memory-tools.ts +234 -0
- package/src/tools/phase-tools.ts +1452 -0
- package/src/tools/pipeline-tools.ts +188 -0
- package/src/tools/registration-tools.ts +450 -0
- package/src/util/.gitkeep +0 -0
- package/src/util/circuit-breaker.ts +193 -0
- package/src/util/config.ts +177 -0
- package/src/util/logger.ts +53 -0
- package/src/util/token-counter.ts +52 -0
- package/src/util/types.ts +710 -0
- package/tests/context/.gitkeep +0 -0
- package/tests/integration/.gitkeep +0 -0
- package/tests/knowledge/.gitkeep +0 -0
- package/tests/learning/.gitkeep +0 -0
- package/tests/pipeline/.gitkeep +0 -0
- package/tests/tools/.gitkeep +0 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +10 -0
- package/vscode-extension/.vscodeignore +7 -0
- package/vscode-extension/README.md +43 -0
- package/vscode-extension/out/edge-collector.js +274 -0
- package/vscode-extension/out/edge-collector.js.map +1 -0
- package/vscode-extension/out/extension.js +264 -0
- package/vscode-extension/out/extension.js.map +1 -0
- package/vscode-extension/out/forge-client.js +318 -0
- package/vscode-extension/out/forge-client.js.map +1 -0
- package/vscode-extension/package-lock.json +59 -0
- package/vscode-extension/package.json +71 -0
- package/vscode-extension/src/edge-collector.ts +320 -0
- package/vscode-extension/src/extension.ts +269 -0
- package/vscode-extension/src/forge-client.ts +364 -0
- package/vscode-extension/tsconfig.json +19 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
// PipelineEngine — the core orchestration layer for Forge projects.
|
|
2
|
+
//
|
|
3
|
+
// The engine owns all project + phase lifecycle operations:
|
|
4
|
+
// - Creating projects
|
|
5
|
+
// - Phase transition validation and persistence (atomic: event + project update)
|
|
6
|
+
// - Trajectory recording (start/step/complete)
|
|
7
|
+
// - Phase output persistence
|
|
8
|
+
// - Claim lifecycle for parallel implementation dispatch
|
|
9
|
+
// - History reconstruction from event log
|
|
10
|
+
// - Cycle count computation from event log
|
|
11
|
+
//
|
|
12
|
+
// All database writes are synchronous (better-sqlite3). External operations
|
|
13
|
+
// (embedding, Qdrant, knowledge extraction) are the responsibility of MCP tool
|
|
14
|
+
// handlers — this engine knows nothing about them.
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import type { PipelineDB } from '../storage/sqlite.js';
|
|
18
|
+
import type {
|
|
19
|
+
Phase,
|
|
20
|
+
PipelinePhase,
|
|
21
|
+
PipelineTier,
|
|
22
|
+
Project,
|
|
23
|
+
ProjectMetadata,
|
|
24
|
+
ProjectRow,
|
|
25
|
+
Trajectory,
|
|
26
|
+
TrajectoryStepMetadata,
|
|
27
|
+
PhaseOutput,
|
|
28
|
+
Claim,
|
|
29
|
+
ClaimStatus,
|
|
30
|
+
ClaimRow,
|
|
31
|
+
PipelineEvent,
|
|
32
|
+
} from '../util/types.js';
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Phase type bridge
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
// The storage layer uses PipelinePhase (agent-role names) in the TypeScript
|
|
39
|
+
// interface types, but the actual DB values are Phase (workflow-stage names).
|
|
40
|
+
// These cast helpers make the boundary explicit without scattering `as unknown`.
|
|
41
|
+
|
|
42
|
+
/** Cast a workflow-stage Phase to the PipelinePhase expected by event factories. */
|
|
43
|
+
function pp(phase: Phase): PipelinePhase {
|
|
44
|
+
return phase as unknown as PipelinePhase;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Cast a PipelinePhase DB value back to a workflow-stage Phase. */
|
|
48
|
+
function wp(phase: PipelinePhase): Phase {
|
|
49
|
+
return phase as unknown as Phase;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Parallel phase pairs
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Phases that can run concurrently. When starting a phase whose parallel
|
|
56
|
+
// sibling is (or leads to) the current phase, we allow the start without
|
|
57
|
+
// advancing currentPhase, so both agents can work and submit independently.
|
|
58
|
+
|
|
59
|
+
const PARALLEL_PAIRS: ReadonlyMap<Phase, Phase> = new Map([
|
|
60
|
+
['architecture', 'design'],
|
|
61
|
+
['design', 'architecture'],
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if `toPhase` can be started as a parallel sibling of the current phase.
|
|
66
|
+
* Returns true when:
|
|
67
|
+
* - `toPhase` has a parallel sibling, AND
|
|
68
|
+
* - `fromPhase` is the sibling itself, OR `fromPhase` can validly transition
|
|
69
|
+
* to the sibling (so both can start from the same predecessor).
|
|
70
|
+
*/
|
|
71
|
+
function isParallelStart(fromPhase: Phase, toPhase: Phase, tier: PipelineTier): boolean {
|
|
72
|
+
const sibling = PARALLEL_PAIRS.get(toPhase);
|
|
73
|
+
if (!sibling) return false;
|
|
74
|
+
return fromPhase === sibling || isValidTransition(fromPhase, sibling, tier);
|
|
75
|
+
}
|
|
76
|
+
import {
|
|
77
|
+
isValidTransition,
|
|
78
|
+
getAvailableTransitions,
|
|
79
|
+
isCycleTransition,
|
|
80
|
+
getCycleKey,
|
|
81
|
+
canCycle,
|
|
82
|
+
getForcedAdvancePhase,
|
|
83
|
+
} from './state-machine.js';
|
|
84
|
+
import {
|
|
85
|
+
phaseTransitionEvent,
|
|
86
|
+
agentStartedEvent,
|
|
87
|
+
agentCompletedEvent,
|
|
88
|
+
projectCreatedEvent,
|
|
89
|
+
projectCompletedEvent,
|
|
90
|
+
claimCreatedEvent,
|
|
91
|
+
claimCompletedEvent,
|
|
92
|
+
claimFailedEvent,
|
|
93
|
+
eventToParams,
|
|
94
|
+
rowToEvent,
|
|
95
|
+
} from './events.js';
|
|
96
|
+
import { logger } from '../util/logger.js';
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Public interfaces
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
export interface CreateProjectOptions {
|
|
103
|
+
name: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
repoId: string;
|
|
106
|
+
tier?: PipelineTier;
|
|
107
|
+
metadata?: ProjectMetadata;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface TransitionResult {
|
|
111
|
+
projectId: string;
|
|
112
|
+
fromPhase: Phase;
|
|
113
|
+
toPhase: Phase;
|
|
114
|
+
trajectoryId: string;
|
|
115
|
+
cycleCounts: Record<string, number>;
|
|
116
|
+
availableTransitions: Phase[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Full project state snapshot — returned by getState(). */
|
|
120
|
+
export interface EngineState {
|
|
121
|
+
project: Project;
|
|
122
|
+
history: PipelineEvent[];
|
|
123
|
+
activeClaims: Claim[];
|
|
124
|
+
cycleCounts: Record<string, number>;
|
|
125
|
+
availableTransitions: Phase[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ProjectState {
|
|
129
|
+
project: Project;
|
|
130
|
+
activeTrajectory: Trajectory | null;
|
|
131
|
+
cycleCounts: Record<string, number>;
|
|
132
|
+
availableTransitions: Phase[];
|
|
133
|
+
activeClaims: Claim[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface PhaseHistory {
|
|
137
|
+
phase: Phase;
|
|
138
|
+
agent: string;
|
|
139
|
+
startedAt: string;
|
|
140
|
+
completedAt?: string;
|
|
141
|
+
success?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ProjectHistory {
|
|
145
|
+
projectId: string;
|
|
146
|
+
events: PipelineEvent[];
|
|
147
|
+
outputs: PhaseOutput[];
|
|
148
|
+
history: PhaseHistory[];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Typed error class so callers can distinguish engine errors from generic ones.
|
|
152
|
+
export type PipelineEngineErrorCode =
|
|
153
|
+
| 'PROJECT_NOT_FOUND'
|
|
154
|
+
| 'PROJECT_NOT_ACTIVE'
|
|
155
|
+
| 'INVALID_TRANSITION'
|
|
156
|
+
| 'CYCLE_LIMIT_EXCEEDED';
|
|
157
|
+
|
|
158
|
+
export class PipelineEngineError extends Error {
|
|
159
|
+
readonly code: PipelineEngineErrorCode;
|
|
160
|
+
readonly details: Record<string, unknown>;
|
|
161
|
+
|
|
162
|
+
constructor(
|
|
163
|
+
code: PipelineEngineErrorCode,
|
|
164
|
+
message: string,
|
|
165
|
+
details: Record<string, unknown> = {},
|
|
166
|
+
) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = 'PipelineEngineError';
|
|
169
|
+
this.code = code;
|
|
170
|
+
this.details = details;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// PipelineEngine
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
export class PipelineEngine {
|
|
179
|
+
constructor(private readonly db: PipelineDB) {}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Project lifecycle
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
createProject(opts: CreateProjectOptions): Project {
|
|
186
|
+
const id = randomUUID();
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const tier: PipelineTier = opts.tier ?? 'full';
|
|
189
|
+
|
|
190
|
+
this.db.run(
|
|
191
|
+
`INSERT INTO projects (id, repo_id, name, description, current_phase, tier, status, created_at, updated_at, metadata)
|
|
192
|
+
VALUES (?, ?, ?, ?, 'idle', ?, 'active', ?, ?, ?)`,
|
|
193
|
+
[
|
|
194
|
+
id,
|
|
195
|
+
opts.repoId,
|
|
196
|
+
opts.name,
|
|
197
|
+
opts.description ?? null,
|
|
198
|
+
tier,
|
|
199
|
+
now,
|
|
200
|
+
now,
|
|
201
|
+
JSON.stringify(opts.metadata ?? {}),
|
|
202
|
+
],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Emit project_created event
|
|
206
|
+
const event = projectCreatedEvent(id, {
|
|
207
|
+
name: opts.name,
|
|
208
|
+
tier,
|
|
209
|
+
repo_id: opts.repoId,
|
|
210
|
+
});
|
|
211
|
+
this.db.run(
|
|
212
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
214
|
+
Object.values(eventToParams(event)),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
logger.info('PipelineEngine: project created', { id, name: opts.name, tier });
|
|
218
|
+
|
|
219
|
+
return this.getProject(id)!;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getProject(projectId: string): Project | null {
|
|
223
|
+
const row = this.db.get<ProjectRow>(
|
|
224
|
+
`SELECT * FROM projects WHERE id = ?`,
|
|
225
|
+
[projectId],
|
|
226
|
+
);
|
|
227
|
+
if (!row) return null;
|
|
228
|
+
return this._rowToProject(row);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Lists projects with optional repo and status filters.
|
|
233
|
+
* Ordered by updated_at DESC (most recently modified first).
|
|
234
|
+
*/
|
|
235
|
+
listProjects(repoId?: string, status?: string): Project[] {
|
|
236
|
+
const conditions: string[] = [];
|
|
237
|
+
const params: unknown[] = [];
|
|
238
|
+
|
|
239
|
+
if (repoId !== undefined) {
|
|
240
|
+
conditions.push('repo_id = ?');
|
|
241
|
+
params.push(repoId);
|
|
242
|
+
}
|
|
243
|
+
if (status !== undefined) {
|
|
244
|
+
conditions.push('status = ?');
|
|
245
|
+
params.push(status);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
249
|
+
const rows = this.db.all<ProjectRow>(
|
|
250
|
+
`SELECT * FROM projects ${where} ORDER BY updated_at DESC`,
|
|
251
|
+
params,
|
|
252
|
+
);
|
|
253
|
+
return rows.map((r) => this._rowToProject(r));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns the most recently updated active project for a repo, or null when
|
|
258
|
+
* there is no active project for that repo.
|
|
259
|
+
*/
|
|
260
|
+
getActiveProject(repoId: string): Project | null {
|
|
261
|
+
const row = this.db.get<ProjectRow>(
|
|
262
|
+
`SELECT * FROM projects WHERE repo_id = ? AND status = 'active' ORDER BY updated_at DESC LIMIT 1`,
|
|
263
|
+
[repoId],
|
|
264
|
+
);
|
|
265
|
+
return row ? this._rowToProject(row) : null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Task-spec core API
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Returns the full current state of a project: project data, full event
|
|
274
|
+
* history, active claims, derived cycle counts, and available transitions.
|
|
275
|
+
*
|
|
276
|
+
* Throws PipelineEngineError with code PROJECT_NOT_FOUND when the project
|
|
277
|
+
* does not exist.
|
|
278
|
+
*/
|
|
279
|
+
getState(projectId: string): EngineState {
|
|
280
|
+
const project = this._requireProject(projectId);
|
|
281
|
+
const history = this.getEventHistory(projectId);
|
|
282
|
+
const activeClaims = this.getActiveClaims(projectId);
|
|
283
|
+
const cycleCounts = this._getCycleCounts(projectId);
|
|
284
|
+
const currentPhase = wp(project.currentPhase);
|
|
285
|
+
const availableTransitions = getAvailableTransitions(currentPhase, project.tier);
|
|
286
|
+
return { project, history, activeClaims, cycleCounts, availableTransitions };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Atomically advances a project to `toPhase` in a single SQLite transaction:
|
|
291
|
+
* 1. Validates the transition (throws on illegal transitions).
|
|
292
|
+
* 2. Checks cycle limits (throws CYCLE_LIMIT_EXCEEDED; details include the
|
|
293
|
+
* forced advance phase so callers can decide how to proceed).
|
|
294
|
+
* 3. Inserts a phase_transition event.
|
|
295
|
+
* 4. Updates projects.current_phase and updated_at.
|
|
296
|
+
* 5. If toPhase === 'completed', marks the project status 'complete' and
|
|
297
|
+
* emits an additional project_completed event.
|
|
298
|
+
*
|
|
299
|
+
* Throws PipelineEngineError for invalid transitions and cycle limit exceeded.
|
|
300
|
+
* All writes are in a single better-sqlite3 transaction.
|
|
301
|
+
*/
|
|
302
|
+
advancePhase(
|
|
303
|
+
projectId: string,
|
|
304
|
+
toPhase: Phase,
|
|
305
|
+
agent?: string,
|
|
306
|
+
payload?: Record<string, unknown>,
|
|
307
|
+
): void {
|
|
308
|
+
const project = this._requireProject(projectId);
|
|
309
|
+
|
|
310
|
+
if (project.status !== 'active') {
|
|
311
|
+
throw new PipelineEngineError(
|
|
312
|
+
'PROJECT_NOT_ACTIVE',
|
|
313
|
+
`Project '${projectId}' has status '${project.status}' and cannot be advanced`,
|
|
314
|
+
{ currentPhase: project.currentPhase, status: project.status },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const fromPhase = wp(project.currentPhase);
|
|
319
|
+
|
|
320
|
+
if (!isValidTransition(fromPhase, toPhase, project.tier)) {
|
|
321
|
+
const available = getAvailableTransitions(fromPhase, project.tier);
|
|
322
|
+
throw new PipelineEngineError(
|
|
323
|
+
'INVALID_TRANSITION',
|
|
324
|
+
`Cannot transition from '${fromPhase}' to '${toPhase}' for tier '${project.tier}'`,
|
|
325
|
+
{ currentPhase: fromPhase, attempted: toPhase, legalTransitions: available },
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Cycle limit check for backward edges.
|
|
330
|
+
if (isCycleTransition(fromPhase, toPhase)) {
|
|
331
|
+
const key = getCycleKey(fromPhase, toPhase)!;
|
|
332
|
+
const cycleCounts = this._getCycleCounts(projectId);
|
|
333
|
+
const count = cycleCounts[key] ?? 0;
|
|
334
|
+
if (!canCycle(fromPhase, toPhase, count)) {
|
|
335
|
+
const forcedPhase = getForcedAdvancePhase(fromPhase, toPhase);
|
|
336
|
+
throw new PipelineEngineError(
|
|
337
|
+
'CYCLE_LIMIT_EXCEEDED',
|
|
338
|
+
`Cycle '${key}' has reached the maximum of ${count} iterations. Force advance to '${forcedPhase ?? 'next'}'`,
|
|
339
|
+
{ cycleKey: key, cycleCount: count, forcedAdvanceTo: forcedPhase },
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
const isTerminal = toPhase === 'completed';
|
|
346
|
+
const newStatus: Project['status'] = isTerminal ? 'complete' : 'active';
|
|
347
|
+
|
|
348
|
+
const transEvent = phaseTransitionEvent(projectId, pp(fromPhase), pp(toPhase), agent ?? null, payload ?? {});
|
|
349
|
+
|
|
350
|
+
this.db.db.transaction(() => {
|
|
351
|
+
// 1. Insert phase_transition event
|
|
352
|
+
this.db.run(
|
|
353
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
354
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
355
|
+
Object.values(eventToParams(transEvent)),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// 2. Update project
|
|
359
|
+
this.db.run(
|
|
360
|
+
`UPDATE projects SET current_phase = ?, updated_at = ?, status = ? WHERE id = ?`,
|
|
361
|
+
[toPhase, now, newStatus, projectId],
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
// 3. Emit project_completed if terminal
|
|
365
|
+
if (isTerminal) {
|
|
366
|
+
const completedEvent = projectCompletedEvent(projectId, { agent: agent ?? null });
|
|
367
|
+
this.db.run(
|
|
368
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
369
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
370
|
+
Object.values(eventToParams(completedEvent)),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
})();
|
|
374
|
+
|
|
375
|
+
logger.info('PipelineEngine: advancePhase', { projectId, from: fromPhase, to: toPhase, agent });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Persists a phase output to phase_outputs. Multiple outputs per phase are
|
|
380
|
+
* allowed (e.g., vision doc, refined vision doc, etc.).
|
|
381
|
+
*
|
|
382
|
+
* Throws PipelineEngineError with code PROJECT_NOT_FOUND when the project
|
|
383
|
+
* does not exist.
|
|
384
|
+
*/
|
|
385
|
+
recordOutput(
|
|
386
|
+
projectId: string,
|
|
387
|
+
phase: Phase,
|
|
388
|
+
outputType: string,
|
|
389
|
+
content: string,
|
|
390
|
+
filePath?: string,
|
|
391
|
+
): void {
|
|
392
|
+
this._requireProject(projectId);
|
|
393
|
+
this.db.run(
|
|
394
|
+
`INSERT INTO phase_outputs (project_id, phase, output_type, content, file_path, created_at)
|
|
395
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
396
|
+
[projectId, phase, outputType, content, filePath ?? null, Date.now()],
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Returns phase outputs for a project, optionally filtered to a specific
|
|
402
|
+
* phase. Ordered by created_at ASC.
|
|
403
|
+
*/
|
|
404
|
+
getOutputs(projectId: string, phase?: Phase): PhaseOutput[] {
|
|
405
|
+
this._requireProject(projectId);
|
|
406
|
+
if (phase !== undefined) {
|
|
407
|
+
return this.db.all<{
|
|
408
|
+
id: number;
|
|
409
|
+
project_id: string;
|
|
410
|
+
phase: string;
|
|
411
|
+
output_type: string;
|
|
412
|
+
content: string;
|
|
413
|
+
file_path: string | null;
|
|
414
|
+
created_at: number;
|
|
415
|
+
}>(
|
|
416
|
+
`SELECT * FROM phase_outputs WHERE project_id = ? AND phase = ? ORDER BY created_at ASC`,
|
|
417
|
+
[projectId, phase],
|
|
418
|
+
).map((r) => this._rowToPhaseOutput(r));
|
|
419
|
+
}
|
|
420
|
+
return this.getAllPhaseOutputs(projectId);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Returns all events for a project ordered by created_at ASC.
|
|
425
|
+
* This is the raw event sourcing log used for history reconstruction.
|
|
426
|
+
*/
|
|
427
|
+
getEventHistory(projectId: string): PipelineEvent[] {
|
|
428
|
+
return this.db.all<{
|
|
429
|
+
id: number;
|
|
430
|
+
project_id: string;
|
|
431
|
+
event_type: string;
|
|
432
|
+
from_phase: string | null;
|
|
433
|
+
to_phase: string | null;
|
|
434
|
+
agent: string | null;
|
|
435
|
+
payload: string;
|
|
436
|
+
created_at: number;
|
|
437
|
+
}>(
|
|
438
|
+
`SELECT * FROM pipeline_events WHERE project_id = ? ORDER BY created_at ASC`,
|
|
439
|
+
[projectId],
|
|
440
|
+
).map(rowToEvent);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Marks a project as 'archived'. Emits a project_completed event with an
|
|
445
|
+
* 'abandoned' flag. Idempotent if already archived.
|
|
446
|
+
*
|
|
447
|
+
* Throws PipelineEngineError with code PROJECT_NOT_FOUND when the project
|
|
448
|
+
* does not exist.
|
|
449
|
+
*/
|
|
450
|
+
abandonProject(projectId: string): void {
|
|
451
|
+
const project = this._requireProject(projectId);
|
|
452
|
+
|
|
453
|
+
if (project.status === 'archived') {
|
|
454
|
+
return; // Idempotent.
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
const completedEvent = projectCompletedEvent(projectId, { abandoned: true });
|
|
459
|
+
|
|
460
|
+
this.db.db.transaction(() => {
|
|
461
|
+
this.db.run(
|
|
462
|
+
`UPDATE projects SET status = 'archived', updated_at = ? WHERE id = ?`,
|
|
463
|
+
[now, projectId],
|
|
464
|
+
);
|
|
465
|
+
this.db.run(
|
|
466
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
467
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
468
|
+
Object.values(eventToParams(completedEvent)),
|
|
469
|
+
);
|
|
470
|
+
})();
|
|
471
|
+
|
|
472
|
+
logger.info('PipelineEngine: project abandoned', { projectId });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------
|
|
476
|
+
// Phase transitions (extended API — used by MCP tools)
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Transition a project to a new phase.
|
|
481
|
+
* Validates the transition against the state machine, emits events,
|
|
482
|
+
* starts a trajectory, and returns the result.
|
|
483
|
+
*/
|
|
484
|
+
startPhase(
|
|
485
|
+
projectId: string,
|
|
486
|
+
toPhase: Phase,
|
|
487
|
+
agent: string,
|
|
488
|
+
extra?: Record<string, unknown>,
|
|
489
|
+
): TransitionResult | { error: string; current_phase: Phase; legal_transitions: Phase[] } {
|
|
490
|
+
const project = this.getProject(projectId);
|
|
491
|
+
if (!project) {
|
|
492
|
+
return { error: 'PROJECT_NOT_FOUND', current_phase: 'idle', legal_transitions: [] };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Cast from PipelinePhase (domain type) to Phase (state-machine type) — the
|
|
496
|
+
// DB stores Phase values in the current_phase column; the Project interface
|
|
497
|
+
// types it as PipelinePhase for legacy compatibility.
|
|
498
|
+
const fromPhase = project.currentPhase as unknown as Phase;
|
|
499
|
+
const tier = project.tier;
|
|
500
|
+
const cycleCounts = this._getCycleCounts(projectId);
|
|
501
|
+
|
|
502
|
+
// Validate transition
|
|
503
|
+
const validTransition = isValidTransition(fromPhase, toPhase, tier);
|
|
504
|
+
const parallelStart = !validTransition && isParallelStart(fromPhase, toPhase, tier);
|
|
505
|
+
|
|
506
|
+
if (!validTransition && !parallelStart) {
|
|
507
|
+
const legal = getAvailableTransitions(fromPhase, tier);
|
|
508
|
+
return {
|
|
509
|
+
error: 'INVALID_TRANSITION',
|
|
510
|
+
current_phase: fromPhase,
|
|
511
|
+
legal_transitions: legal,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// For parallel siblings (architecture + design), don't change currentPhase —
|
|
516
|
+
// both agents run concurrently and the submit handlers coordinate advancement.
|
|
517
|
+
// Skip only when the current phase IS the sibling (both are running from the
|
|
518
|
+
// same starting point) or when we allowed a parallelStart.
|
|
519
|
+
const sibling = PARALLEL_PAIRS.get(toPhase);
|
|
520
|
+
const skipPhaseUpdate = parallelStart || (validTransition && sibling !== undefined && fromPhase === sibling);
|
|
521
|
+
|
|
522
|
+
// Check cycle limits (only for non-parallel forward transitions)
|
|
523
|
+
if (!skipPhaseUpdate && isCycleTransition(fromPhase, toPhase)) {
|
|
524
|
+
const key = getCycleKey(fromPhase, toPhase)!;
|
|
525
|
+
const count = cycleCounts[key] ?? 0;
|
|
526
|
+
if (!canCycle(fromPhase, toPhase, count)) {
|
|
527
|
+
const forcedPhase = getForcedAdvancePhase(fromPhase, toPhase);
|
|
528
|
+
logger.warn('PipelineEngine: cycle limit exceeded, forcing advance', {
|
|
529
|
+
projectId,
|
|
530
|
+
from: fromPhase,
|
|
531
|
+
to: toPhase,
|
|
532
|
+
forced: forcedPhase,
|
|
533
|
+
count,
|
|
534
|
+
});
|
|
535
|
+
// Force advance to the prescribed phase instead
|
|
536
|
+
if (forcedPhase) {
|
|
537
|
+
return this.startPhase(projectId, forcedPhase, agent, {
|
|
538
|
+
...extra,
|
|
539
|
+
forced: true,
|
|
540
|
+
reason: 'cycle_limit_exceeded',
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Increment cycle counter
|
|
545
|
+
cycleCounts[key] = (cycleCounts[key] ?? 0) + 1;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
|
|
550
|
+
// Update project phase (skip for parallel siblings)
|
|
551
|
+
if (!skipPhaseUpdate) {
|
|
552
|
+
this.db.run(
|
|
553
|
+
`UPDATE projects SET current_phase = ?, updated_at = ? WHERE id = ?`,
|
|
554
|
+
[toPhase, now, projectId],
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Emit transition event (bridge Phase -> PipelinePhase for event factory)
|
|
559
|
+
const transEvent = phaseTransitionEvent(projectId, pp(fromPhase), pp(toPhase), agent, extra ?? {});
|
|
560
|
+
this.db.run(
|
|
561
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
562
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
563
|
+
Object.values(eventToParams(transEvent)),
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// Emit agent_started event
|
|
567
|
+
const startedEvent = agentStartedEvent(projectId, pp(toPhase), agent, extra ?? {});
|
|
568
|
+
this.db.run(
|
|
569
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
570
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
571
|
+
Object.values(eventToParams(startedEvent)),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// Start trajectory
|
|
575
|
+
const trajectoryId = randomUUID();
|
|
576
|
+
this.db.run(
|
|
577
|
+
`INSERT INTO trajectories (id, project_id, phase, agent, status, started_at)
|
|
578
|
+
VALUES (?, ?, ?, ?, 'active', ?)`,
|
|
579
|
+
[trajectoryId, projectId, toPhase, agent, now],
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
logger.info('PipelineEngine: phase started', { projectId, from: fromPhase, to: toPhase, agent, parallel: skipPhaseUpdate });
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
projectId,
|
|
586
|
+
fromPhase,
|
|
587
|
+
toPhase,
|
|
588
|
+
trajectoryId,
|
|
589
|
+
cycleCounts,
|
|
590
|
+
availableTransitions: getAvailableTransitions(toPhase, tier),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Record completion of a phase: persist output, complete the trajectory,
|
|
596
|
+
* and optionally advance the state machine.
|
|
597
|
+
*/
|
|
598
|
+
completePhase(
|
|
599
|
+
projectId: string,
|
|
600
|
+
phase: Phase,
|
|
601
|
+
outputType: string,
|
|
602
|
+
content: string,
|
|
603
|
+
opts: {
|
|
604
|
+
agent: string;
|
|
605
|
+
filePath?: string;
|
|
606
|
+
advanceTo?: Phase;
|
|
607
|
+
trajectoryId?: string;
|
|
608
|
+
qualityScore?: number;
|
|
609
|
+
metadata?: Record<string, unknown>;
|
|
610
|
+
},
|
|
611
|
+
): { outputId: number; nextPhase: Phase | null } {
|
|
612
|
+
const project = this.getProject(projectId);
|
|
613
|
+
if (!project) {
|
|
614
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
|
|
619
|
+
// Persist phase output
|
|
620
|
+
const result = this.db.run(
|
|
621
|
+
`INSERT INTO phase_outputs (project_id, phase, output_type, content, file_path, created_at)
|
|
622
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
623
|
+
[projectId, phase, outputType, content, opts.filePath ?? null, now],
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// Complete active trajectory for this phase
|
|
627
|
+
if (opts.trajectoryId) {
|
|
628
|
+
this.db.run(
|
|
629
|
+
`UPDATE trajectories SET status = 'complete', completed_at = ?, success = 1 WHERE id = ?`,
|
|
630
|
+
[now, opts.trajectoryId],
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
// Record trajectory step for the output
|
|
634
|
+
this.db.run(
|
|
635
|
+
`INSERT INTO trajectory_steps (trajectory_id, action, result, quality_score, metadata, created_at)
|
|
636
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
637
|
+
[
|
|
638
|
+
opts.trajectoryId,
|
|
639
|
+
`submit_${phase}`,
|
|
640
|
+
content.slice(0, 500),
|
|
641
|
+
opts.qualityScore ?? null,
|
|
642
|
+
JSON.stringify(opts.metadata ?? {}),
|
|
643
|
+
now,
|
|
644
|
+
],
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Emit agent_completed event
|
|
649
|
+
const completedEvent = agentCompletedEvent(projectId, pp(phase), opts.agent, {
|
|
650
|
+
output_type: outputType,
|
|
651
|
+
content_length: content.length,
|
|
652
|
+
...opts.metadata,
|
|
653
|
+
});
|
|
654
|
+
this.db.run(
|
|
655
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
656
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
657
|
+
Object.values(eventToParams(completedEvent)),
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// Advance state if requested
|
|
661
|
+
if (opts.advanceTo) {
|
|
662
|
+
this.db.run(
|
|
663
|
+
`UPDATE projects SET current_phase = ?, updated_at = ? WHERE id = ?`,
|
|
664
|
+
[opts.advanceTo, now, projectId],
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
const transEvent = phaseTransitionEvent(projectId, pp(phase), pp(opts.advanceTo), opts.agent, {});
|
|
668
|
+
this.db.run(
|
|
669
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
670
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
671
|
+
Object.values(eventToParams(transEvent)),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
logger.info('PipelineEngine: phase completed, advanced', {
|
|
675
|
+
projectId,
|
|
676
|
+
from: phase,
|
|
677
|
+
to: opts.advanceTo,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
outputId: Number(result.lastInsertRowid),
|
|
683
|
+
nextPhase: opts.advanceTo ?? null,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ---------------------------------------------------------------------------
|
|
688
|
+
// Trajectory helpers
|
|
689
|
+
// ---------------------------------------------------------------------------
|
|
690
|
+
|
|
691
|
+
startTrajectory(projectId: string, phase: Phase, agent: string): string {
|
|
692
|
+
const id = randomUUID();
|
|
693
|
+
this.db.run(
|
|
694
|
+
`INSERT INTO trajectories (id, project_id, phase, agent, status, started_at)
|
|
695
|
+
VALUES (?, ?, ?, ?, 'active', ?)`,
|
|
696
|
+
[id, projectId, phase, agent, Date.now()],
|
|
697
|
+
);
|
|
698
|
+
return id;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
recordTrajectoryStep(
|
|
702
|
+
trajectoryId: string,
|
|
703
|
+
action: string,
|
|
704
|
+
result?: string,
|
|
705
|
+
qualityScore?: number,
|
|
706
|
+
metadata?: TrajectoryStepMetadata,
|
|
707
|
+
): void {
|
|
708
|
+
this.db.run(
|
|
709
|
+
`INSERT INTO trajectory_steps (trajectory_id, action, result, quality_score, metadata, created_at)
|
|
710
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
711
|
+
[
|
|
712
|
+
trajectoryId,
|
|
713
|
+
action,
|
|
714
|
+
result ?? null,
|
|
715
|
+
qualityScore ?? null,
|
|
716
|
+
JSON.stringify(metadata ?? {}),
|
|
717
|
+
Date.now(),
|
|
718
|
+
],
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
completeTrajectory(
|
|
723
|
+
trajectoryId: string,
|
|
724
|
+
success: boolean,
|
|
725
|
+
feedback?: string,
|
|
726
|
+
): void {
|
|
727
|
+
this.db.run(
|
|
728
|
+
`UPDATE trajectories SET status = ?, completed_at = ?, success = ?, feedback = ? WHERE id = ?`,
|
|
729
|
+
[success ? 'complete' : 'failed', Date.now(), success ? 1 : 0, feedback ?? null, trajectoryId],
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// Claims (parallel implementation dispatch)
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
createClaim(projectId: string, moduleName: string, agent: string): Claim {
|
|
738
|
+
const id = randomUUID();
|
|
739
|
+
const now = Date.now();
|
|
740
|
+
this.db.run(
|
|
741
|
+
`INSERT INTO claims (id, project_id, module_name, agent, status, claimed_at)
|
|
742
|
+
VALUES (?, ?, ?, ?, 'claimed', ?)`,
|
|
743
|
+
[id, projectId, moduleName, agent, now],
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
const event = claimCreatedEvent(projectId, id, moduleName, agent);
|
|
747
|
+
this.db.run(
|
|
748
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
749
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
750
|
+
Object.values(eventToParams(event)),
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
id,
|
|
755
|
+
projectId,
|
|
756
|
+
moduleName,
|
|
757
|
+
agent,
|
|
758
|
+
branch: null,
|
|
759
|
+
status: 'claimed',
|
|
760
|
+
claimedAt: now,
|
|
761
|
+
completedAt: null,
|
|
762
|
+
result: null,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
completeClaim(
|
|
767
|
+
claimId: string,
|
|
768
|
+
result?: string,
|
|
769
|
+
): { remainingCount: number; allComplete: boolean } {
|
|
770
|
+
const row = this.db.get<ClaimRow>(`SELECT * FROM claims WHERE id = ?`, [claimId]);
|
|
771
|
+
if (!row) throw new Error(`Claim not found: ${claimId}`);
|
|
772
|
+
|
|
773
|
+
const now = Date.now();
|
|
774
|
+
this.db.run(
|
|
775
|
+
`UPDATE claims SET status = 'complete', completed_at = ?, result = ? WHERE id = ?`,
|
|
776
|
+
[now, result ?? null, claimId],
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const event = claimCompletedEvent(row.project_id, claimId, row.module_name, row.agent, result);
|
|
780
|
+
this.db.run(
|
|
781
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
782
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
783
|
+
Object.values(eventToParams(event)),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const remaining = this.db.all<{ id: string }>(
|
|
787
|
+
`SELECT id FROM claims WHERE project_id = ? AND status NOT IN ('complete', 'failed')`,
|
|
788
|
+
[row.project_id],
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
remainingCount: remaining.length,
|
|
793
|
+
allComplete: remaining.length === 0,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
failClaim(claimId: string, reason: string): void {
|
|
798
|
+
const row = this.db.get<ClaimRow>(`SELECT * FROM claims WHERE id = ?`, [claimId]);
|
|
799
|
+
if (!row) throw new Error(`Claim not found: ${claimId}`);
|
|
800
|
+
|
|
801
|
+
this.db.run(
|
|
802
|
+
`UPDATE claims SET status = 'failed', completed_at = ? WHERE id = ?`,
|
|
803
|
+
[Date.now(), claimId],
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const event = claimFailedEvent(row.project_id, claimId, row.module_name, row.agent, reason);
|
|
807
|
+
this.db.run(
|
|
808
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
809
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
810
|
+
Object.values(eventToParams(event)),
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
getActiveClaims(projectId: string): Claim[] {
|
|
815
|
+
const rows = this.db.all<ClaimRow>(
|
|
816
|
+
`SELECT * FROM claims WHERE project_id = ? AND status NOT IN ('complete', 'failed')`,
|
|
817
|
+
[projectId],
|
|
818
|
+
);
|
|
819
|
+
return rows.map((r) => this._rowToClaim(r));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
getClaims(projectId: string, status?: ClaimStatus): Claim[] {
|
|
823
|
+
if (status !== undefined) {
|
|
824
|
+
const rows = this.db.all<ClaimRow>(
|
|
825
|
+
`SELECT * FROM claims WHERE project_id = ? AND status = ? ORDER BY claimed_at ASC`,
|
|
826
|
+
[projectId, status],
|
|
827
|
+
);
|
|
828
|
+
return rows.map((r) => this._rowToClaim(r));
|
|
829
|
+
}
|
|
830
|
+
const rows = this.db.all<ClaimRow>(
|
|
831
|
+
`SELECT * FROM claims WHERE project_id = ? ORDER BY claimed_at ASC`,
|
|
832
|
+
[projectId],
|
|
833
|
+
);
|
|
834
|
+
return rows.map((r) => this._rowToClaim(r));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
getClaim(claimId: string): Claim | null {
|
|
838
|
+
const row = this.db.get<ClaimRow>(`SELECT * FROM claims WHERE id = ?`, [claimId]);
|
|
839
|
+
return row ? this._rowToClaim(row) : null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
getClaimByModule(projectId: string, moduleName: string): Claim | null {
|
|
843
|
+
const row = this.db.get<ClaimRow>(
|
|
844
|
+
`SELECT * FROM claims WHERE project_id = ? AND module_name = ? ORDER BY claimed_at DESC LIMIT 1`,
|
|
845
|
+
[projectId, moduleName],
|
|
846
|
+
);
|
|
847
|
+
return row ? this._rowToClaim(row) : null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// ---------------------------------------------------------------------------
|
|
851
|
+
// Phase outputs
|
|
852
|
+
// ---------------------------------------------------------------------------
|
|
853
|
+
|
|
854
|
+
getPhaseOutput(projectId: string, phase: Phase, outputType?: string): PhaseOutput | null {
|
|
855
|
+
const sql = outputType
|
|
856
|
+
? `SELECT * FROM phase_outputs WHERE project_id = ? AND phase = ? AND output_type = ? ORDER BY created_at DESC LIMIT 1`
|
|
857
|
+
: `SELECT * FROM phase_outputs WHERE project_id = ? AND phase = ? ORDER BY created_at DESC LIMIT 1`;
|
|
858
|
+
const params = outputType ? [projectId, phase, outputType] : [projectId, phase];
|
|
859
|
+
const row = this.db.get<{
|
|
860
|
+
id: number;
|
|
861
|
+
project_id: string;
|
|
862
|
+
phase: string;
|
|
863
|
+
output_type: string;
|
|
864
|
+
content: string;
|
|
865
|
+
file_path: string | null;
|
|
866
|
+
created_at: number;
|
|
867
|
+
}>(sql, params);
|
|
868
|
+
if (!row) return null;
|
|
869
|
+
return this._rowToPhaseOutput(row);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Retrieve metadata stored in the agent_completed event for a phase.
|
|
874
|
+
* This includes flags like requires_qa, requires_designer, modules, etc.
|
|
875
|
+
* that are passed via opts.metadata in completePhase().
|
|
876
|
+
*/
|
|
877
|
+
getPhaseMetadata(projectId: string, phase: Phase): Record<string, unknown> | null {
|
|
878
|
+
const row = this.db.get<{ payload: string }>(
|
|
879
|
+
`SELECT payload FROM pipeline_events
|
|
880
|
+
WHERE project_id = ? AND event_type = 'agent_completed' AND from_phase = ?
|
|
881
|
+
ORDER BY created_at DESC LIMIT 1`,
|
|
882
|
+
[projectId, phase],
|
|
883
|
+
);
|
|
884
|
+
if (!row) return null;
|
|
885
|
+
try {
|
|
886
|
+
return JSON.parse(row.payload) as Record<string, unknown>;
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Record a broadcast event in the pipeline_events table.
|
|
894
|
+
* Used by phase tools to auto-broadcast module completions for swarm coordination.
|
|
895
|
+
*/
|
|
896
|
+
recordBroadcast(
|
|
897
|
+
projectId: string,
|
|
898
|
+
content: string,
|
|
899
|
+
severity: 'info' | 'warning' | 'critical' = 'info',
|
|
900
|
+
): void {
|
|
901
|
+
this.db.run(
|
|
902
|
+
`INSERT INTO pipeline_events (project_id, event_type, from_phase, to_phase, agent, payload, created_at)
|
|
903
|
+
VALUES (?, ?, NULL, NULL, NULL, ?, ?)`,
|
|
904
|
+
[
|
|
905
|
+
projectId,
|
|
906
|
+
'agent_broadcast',
|
|
907
|
+
JSON.stringify({ content, severity }),
|
|
908
|
+
Date.now(),
|
|
909
|
+
],
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
getAllPhaseOutputs(projectId: string): PhaseOutput[] {
|
|
914
|
+
const rows = this.db.all<{
|
|
915
|
+
id: number;
|
|
916
|
+
project_id: string;
|
|
917
|
+
phase: string;
|
|
918
|
+
output_type: string;
|
|
919
|
+
content: string;
|
|
920
|
+
file_path: string | null;
|
|
921
|
+
created_at: number;
|
|
922
|
+
}>(`SELECT * FROM phase_outputs WHERE project_id = ? ORDER BY created_at ASC`, [projectId]);
|
|
923
|
+
return rows.map((r) => this._rowToPhaseOutput(r));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ---------------------------------------------------------------------------
|
|
927
|
+
// History
|
|
928
|
+
// ---------------------------------------------------------------------------
|
|
929
|
+
|
|
930
|
+
getHistory(projectId: string): ProjectHistory {
|
|
931
|
+
const events = this.db.all<{
|
|
932
|
+
id: number;
|
|
933
|
+
project_id: string;
|
|
934
|
+
event_type: string;
|
|
935
|
+
from_phase: string | null;
|
|
936
|
+
to_phase: string | null;
|
|
937
|
+
agent: string | null;
|
|
938
|
+
payload: string;
|
|
939
|
+
created_at: number;
|
|
940
|
+
}>(`SELECT * FROM pipeline_events WHERE project_id = ? ORDER BY created_at ASC`, [projectId]);
|
|
941
|
+
|
|
942
|
+
const outputs = this.getAllPhaseOutputs(projectId);
|
|
943
|
+
|
|
944
|
+
// Build phase history from agent_started / agent_completed pairs.
|
|
945
|
+
// ev.fromPhase is PipelinePhase (domain type), PhaseHistory.phase is Phase.
|
|
946
|
+
const history: PhaseHistory[] = [];
|
|
947
|
+
const startedMap = new Map<string, { phase: Phase; agent: string; startedAt: number }>();
|
|
948
|
+
|
|
949
|
+
for (const row of events) {
|
|
950
|
+
const ev = rowToEvent(row);
|
|
951
|
+
if (ev.eventType === 'agent_started' && ev.fromPhase) {
|
|
952
|
+
startedMap.set(`${ev.fromPhase}-${ev.agent}`, {
|
|
953
|
+
phase: wp(ev.fromPhase),
|
|
954
|
+
agent: ev.agent ?? 'unknown',
|
|
955
|
+
startedAt: ev.createdAt,
|
|
956
|
+
});
|
|
957
|
+
} else if (ev.eventType === 'agent_completed' && ev.fromPhase) {
|
|
958
|
+
const key = `${ev.fromPhase}-${ev.agent}`;
|
|
959
|
+
const started = startedMap.get(key);
|
|
960
|
+
if (started) {
|
|
961
|
+
history.push({
|
|
962
|
+
phase: started.phase,
|
|
963
|
+
agent: started.agent,
|
|
964
|
+
startedAt: new Date(started.startedAt).toISOString(),
|
|
965
|
+
completedAt: new Date(ev.createdAt).toISOString(),
|
|
966
|
+
success: true,
|
|
967
|
+
});
|
|
968
|
+
startedMap.delete(key);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Flush any in-progress phases
|
|
974
|
+
for (const [, started] of startedMap) {
|
|
975
|
+
history.push({
|
|
976
|
+
phase: started.phase,
|
|
977
|
+
agent: started.agent,
|
|
978
|
+
startedAt: new Date(started.startedAt).toISOString(),
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
return {
|
|
983
|
+
projectId,
|
|
984
|
+
events: events.map(rowToEvent),
|
|
985
|
+
outputs,
|
|
986
|
+
history,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// ---------------------------------------------------------------------------
|
|
991
|
+
// Cycle tracking
|
|
992
|
+
// ---------------------------------------------------------------------------
|
|
993
|
+
|
|
994
|
+
getCycleCounts(projectId: string): Record<string, number> {
|
|
995
|
+
return this._getCycleCounts(projectId);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private _getCycleCounts(projectId: string): Record<string, number> {
|
|
999
|
+
const rows = this.db.all<{
|
|
1000
|
+
from_phase: string;
|
|
1001
|
+
to_phase: string;
|
|
1002
|
+
count: number;
|
|
1003
|
+
}>(
|
|
1004
|
+
`SELECT from_phase, to_phase, COUNT(*) as count
|
|
1005
|
+
FROM pipeline_events
|
|
1006
|
+
WHERE project_id = ? AND event_type = 'phase_transition' AND from_phase IS NOT NULL AND to_phase IS NOT NULL
|
|
1007
|
+
GROUP BY from_phase, to_phase`,
|
|
1008
|
+
[projectId],
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
const counts: Record<string, number> = {};
|
|
1012
|
+
for (const row of rows) {
|
|
1013
|
+
const key = `${row.from_phase}_to_${row.to_phase}`;
|
|
1014
|
+
// Only include recognised cycle keys
|
|
1015
|
+
if (getCycleKey(row.from_phase as Phase, row.to_phase as Phase)) {
|
|
1016
|
+
counts[key] = row.count;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return counts;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ---------------------------------------------------------------------------
|
|
1023
|
+
// Internal helpers
|
|
1024
|
+
// ---------------------------------------------------------------------------
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Retrieves a project by id and throws a typed PipelineEngineError when not
|
|
1028
|
+
* found. Used by every method that requires an existing project.
|
|
1029
|
+
*/
|
|
1030
|
+
private _requireProject(projectId: string): Project {
|
|
1031
|
+
const row = this.db.get<ProjectRow>(
|
|
1032
|
+
`SELECT * FROM projects WHERE id = ?`,
|
|
1033
|
+
[projectId],
|
|
1034
|
+
);
|
|
1035
|
+
if (!row) {
|
|
1036
|
+
throw new PipelineEngineError(
|
|
1037
|
+
'PROJECT_NOT_FOUND',
|
|
1038
|
+
`Project '${projectId}' does not exist`,
|
|
1039
|
+
{ projectId },
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
return this._rowToProject(row);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ---------------------------------------------------------------------------
|
|
1046
|
+
// Row mappers
|
|
1047
|
+
// ---------------------------------------------------------------------------
|
|
1048
|
+
|
|
1049
|
+
private _rowToProject(row: ProjectRow): Project {
|
|
1050
|
+
let metadata: ProjectMetadata;
|
|
1051
|
+
try {
|
|
1052
|
+
metadata = JSON.parse(row.metadata) as ProjectMetadata;
|
|
1053
|
+
} catch {
|
|
1054
|
+
metadata = {};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
id: row.id,
|
|
1059
|
+
repoId: row.repo_id,
|
|
1060
|
+
name: row.name,
|
|
1061
|
+
description: row.description,
|
|
1062
|
+
// The DB stores Phase (workflow-stage) values in current_phase, but
|
|
1063
|
+
// Project.currentPhase is typed as PipelinePhase. We bridge here so
|
|
1064
|
+
// callers use wp() to get back to Phase when needed.
|
|
1065
|
+
currentPhase: row.current_phase as unknown as PipelinePhase,
|
|
1066
|
+
tier: row.tier,
|
|
1067
|
+
status: row.status,
|
|
1068
|
+
createdAt: row.created_at,
|
|
1069
|
+
updatedAt: row.updated_at,
|
|
1070
|
+
metadata,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private _rowToClaim(row: ClaimRow): Claim {
|
|
1075
|
+
return {
|
|
1076
|
+
id: row.id,
|
|
1077
|
+
projectId: row.project_id,
|
|
1078
|
+
moduleName: row.module_name,
|
|
1079
|
+
agent: row.agent,
|
|
1080
|
+
branch: row.branch,
|
|
1081
|
+
status: row.status,
|
|
1082
|
+
claimedAt: row.claimed_at,
|
|
1083
|
+
completedAt: row.completed_at,
|
|
1084
|
+
result: row.result,
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private _rowToPhaseOutput(row: {
|
|
1089
|
+
id: number;
|
|
1090
|
+
project_id: string;
|
|
1091
|
+
phase: string;
|
|
1092
|
+
output_type: string;
|
|
1093
|
+
content: string;
|
|
1094
|
+
file_path: string | null;
|
|
1095
|
+
created_at: number;
|
|
1096
|
+
}): PhaseOutput {
|
|
1097
|
+
return {
|
|
1098
|
+
id: row.id,
|
|
1099
|
+
projectId: row.project_id,
|
|
1100
|
+
// Bridge: Phase values stored in DB, PhaseOutput.phase typed as PipelinePhase.
|
|
1101
|
+
phase: row.phase as unknown as PipelinePhase,
|
|
1102
|
+
outputType: row.output_type,
|
|
1103
|
+
content: row.content,
|
|
1104
|
+
filePath: row.file_path,
|
|
1105
|
+
createdAt: row.created_at,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Expose getTrajectory for phase tools
|
|
1110
|
+
getActiveTrajectory(projectId: string, phase: Phase): Trajectory | null {
|
|
1111
|
+
const row = this.db.get<{
|
|
1112
|
+
id: string;
|
|
1113
|
+
project_id: string;
|
|
1114
|
+
phase: string;
|
|
1115
|
+
agent: string;
|
|
1116
|
+
status: string;
|
|
1117
|
+
started_at: number;
|
|
1118
|
+
completed_at: number | null;
|
|
1119
|
+
success: number | null;
|
|
1120
|
+
feedback: string | null;
|
|
1121
|
+
}>(
|
|
1122
|
+
`SELECT * FROM trajectories WHERE project_id = ? AND phase = ? AND status = 'active' ORDER BY started_at DESC LIMIT 1`,
|
|
1123
|
+
[projectId, phase],
|
|
1124
|
+
);
|
|
1125
|
+
if (!row) return null;
|
|
1126
|
+
return {
|
|
1127
|
+
id: row.id,
|
|
1128
|
+
projectId: row.project_id,
|
|
1129
|
+
// Bridge: Phase stored in DB, Trajectory.phase typed as PipelinePhase.
|
|
1130
|
+
phase: row.phase as unknown as PipelinePhase,
|
|
1131
|
+
agent: row.agent,
|
|
1132
|
+
status: row.status as 'active' | 'complete' | 'failed' | 'abandoned',
|
|
1133
|
+
startedAt: row.started_at,
|
|
1134
|
+
completedAt: row.completed_at,
|
|
1135
|
+
success: row.success === null ? null : row.success === 1,
|
|
1136
|
+
feedback: row.feedback,
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
}
|