@agile-vibe-coding/avc 0.1.0 → 0.2.3
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/README.md +2 -0
- package/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +129 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/epic-story-decomposer.md +280 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +79 -0
- package/cli/agents/mission-scope-validator.md +112 -0
- package/cli/agents/project-context-extractor.md +107 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/solver-epic-api.json +15 -0
- package/cli/agents/solver-epic-api.md +39 -0
- package/cli/agents/solver-epic-backend.json +15 -0
- package/cli/agents/solver-epic-backend.md +39 -0
- package/cli/agents/solver-epic-cloud.json +15 -0
- package/cli/agents/solver-epic-cloud.md +39 -0
- package/cli/agents/solver-epic-data.json +15 -0
- package/cli/agents/solver-epic-data.md +39 -0
- package/cli/agents/solver-epic-database.json +15 -0
- package/cli/agents/solver-epic-database.md +39 -0
- package/cli/agents/solver-epic-developer.json +15 -0
- package/cli/agents/solver-epic-developer.md +39 -0
- package/cli/agents/solver-epic-devops.json +15 -0
- package/cli/agents/solver-epic-devops.md +39 -0
- package/cli/agents/solver-epic-frontend.json +15 -0
- package/cli/agents/solver-epic-frontend.md +39 -0
- package/cli/agents/solver-epic-mobile.json +15 -0
- package/cli/agents/solver-epic-mobile.md +39 -0
- package/cli/agents/solver-epic-qa.json +15 -0
- package/cli/agents/solver-epic-qa.md +39 -0
- package/cli/agents/solver-epic-security.json +15 -0
- package/cli/agents/solver-epic-security.md +39 -0
- package/cli/agents/solver-epic-solution-architect.json +15 -0
- package/cli/agents/solver-epic-solution-architect.md +39 -0
- package/cli/agents/solver-epic-test-architect.json +15 -0
- package/cli/agents/solver-epic-test-architect.md +39 -0
- package/cli/agents/solver-epic-ui.json +15 -0
- package/cli/agents/solver-epic-ui.md +39 -0
- package/cli/agents/solver-epic-ux.json +15 -0
- package/cli/agents/solver-epic-ux.md +39 -0
- package/cli/agents/solver-story-api.json +15 -0
- package/cli/agents/solver-story-api.md +39 -0
- package/cli/agents/solver-story-backend.json +15 -0
- package/cli/agents/solver-story-backend.md +39 -0
- package/cli/agents/solver-story-cloud.json +15 -0
- package/cli/agents/solver-story-cloud.md +39 -0
- package/cli/agents/solver-story-data.json +15 -0
- package/cli/agents/solver-story-data.md +39 -0
- package/cli/agents/solver-story-database.json +15 -0
- package/cli/agents/solver-story-database.md +39 -0
- package/cli/agents/solver-story-developer.json +15 -0
- package/cli/agents/solver-story-developer.md +39 -0
- package/cli/agents/solver-story-devops.json +15 -0
- package/cli/agents/solver-story-devops.md +39 -0
- package/cli/agents/solver-story-frontend.json +15 -0
- package/cli/agents/solver-story-frontend.md +39 -0
- package/cli/agents/solver-story-mobile.json +15 -0
- package/cli/agents/solver-story-mobile.md +39 -0
- package/cli/agents/solver-story-qa.json +15 -0
- package/cli/agents/solver-story-qa.md +39 -0
- package/cli/agents/solver-story-security.json +15 -0
- package/cli/agents/solver-story-security.md +39 -0
- package/cli/agents/solver-story-solution-architect.json +15 -0
- package/cli/agents/solver-story-solution-architect.md +39 -0
- package/cli/agents/solver-story-test-architect.json +15 -0
- package/cli/agents/solver-story-test-architect.md +39 -0
- package/cli/agents/solver-story-ui.json +15 -0
- package/cli/agents/solver-story-ui.md +39 -0
- package/cli/agents/solver-story-ux.json +15 -0
- package/cli/agents/solver-story-ux.md +39 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +152 -0
- package/cli/agents/validator-documentation.md +453 -0
- package/cli/agents/validator-epic-api.json +93 -0
- package/cli/agents/validator-epic-api.md +137 -0
- package/cli/agents/validator-epic-backend.json +93 -0
- package/cli/agents/validator-epic-backend.md +130 -0
- package/cli/agents/validator-epic-cloud.json +93 -0
- package/cli/agents/validator-epic-cloud.md +137 -0
- package/cli/agents/validator-epic-data.json +93 -0
- package/cli/agents/validator-epic-data.md +130 -0
- package/cli/agents/validator-epic-database.json +93 -0
- package/cli/agents/validator-epic-database.md +137 -0
- package/cli/agents/validator-epic-developer.json +74 -0
- package/cli/agents/validator-epic-developer.md +153 -0
- package/cli/agents/validator-epic-devops.json +74 -0
- package/cli/agents/validator-epic-devops.md +153 -0
- package/cli/agents/validator-epic-frontend.json +74 -0
- package/cli/agents/validator-epic-frontend.md +153 -0
- package/cli/agents/validator-epic-mobile.json +93 -0
- package/cli/agents/validator-epic-mobile.md +130 -0
- package/cli/agents/validator-epic-qa.json +93 -0
- package/cli/agents/validator-epic-qa.md +130 -0
- package/cli/agents/validator-epic-security.json +74 -0
- package/cli/agents/validator-epic-security.md +154 -0
- package/cli/agents/validator-epic-solution-architect.json +74 -0
- package/cli/agents/validator-epic-solution-architect.md +156 -0
- package/cli/agents/validator-epic-test-architect.json +93 -0
- package/cli/agents/validator-epic-test-architect.md +130 -0
- package/cli/agents/validator-epic-ui.json +93 -0
- package/cli/agents/validator-epic-ui.md +130 -0
- package/cli/agents/validator-epic-ux.json +93 -0
- package/cli/agents/validator-epic-ux.md +130 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/agents/validator-story-api.json +104 -0
- package/cli/agents/validator-story-api.md +152 -0
- package/cli/agents/validator-story-backend.json +104 -0
- package/cli/agents/validator-story-backend.md +152 -0
- package/cli/agents/validator-story-cloud.json +104 -0
- package/cli/agents/validator-story-cloud.md +152 -0
- package/cli/agents/validator-story-data.json +104 -0
- package/cli/agents/validator-story-data.md +152 -0
- package/cli/agents/validator-story-database.json +104 -0
- package/cli/agents/validator-story-database.md +152 -0
- package/cli/agents/validator-story-developer.json +104 -0
- package/cli/agents/validator-story-developer.md +152 -0
- package/cli/agents/validator-story-devops.json +104 -0
- package/cli/agents/validator-story-devops.md +152 -0
- package/cli/agents/validator-story-frontend.json +104 -0
- package/cli/agents/validator-story-frontend.md +152 -0
- package/cli/agents/validator-story-mobile.json +104 -0
- package/cli/agents/validator-story-mobile.md +152 -0
- package/cli/agents/validator-story-qa.json +104 -0
- package/cli/agents/validator-story-qa.md +152 -0
- package/cli/agents/validator-story-security.json +104 -0
- package/cli/agents/validator-story-security.md +152 -0
- package/cli/agents/validator-story-solution-architect.json +104 -0
- package/cli/agents/validator-story-solution-architect.md +152 -0
- package/cli/agents/validator-story-test-architect.json +104 -0
- package/cli/agents/validator-story-test-architect.md +152 -0
- package/cli/agents/validator-story-ui.json +104 -0
- package/cli/agents/validator-story-ui.md +152 -0
- package/cli/agents/validator-story-ux.json +104 -0
- package/cli/agents/validator-story-ux.md +152 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/build-docs.js +298 -0
- package/cli/ceremony-history.js +369 -0
- package/cli/command-logger.js +245 -0
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +1174 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/index.js +3 -25
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1765 -100
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +109 -0
- package/cli/llm-gemini.js +115 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +300 -0
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- package/cli/logger.js +32 -5
- package/cli/message-constants.js +58 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +297 -0
- package/cli/model-pricing.js +169 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +269 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +332 -0
- package/cli/repl-ink.js +5840 -504
- package/cli/repl-old.js +4 -4
- package/cli/seed-processor.js +792 -0
- package/cli/sprint-planning-processor.js +1813 -0
- package/cli/template-processor.js +2306 -108
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +34 -0
- package/cli/token-tracker.js +520 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +605 -0
- package/cli/verification-tracker.js +563 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
- package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +622 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +353 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +118 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +401 -0
- package/kanban/client/src/lib/status-grouping.js +144 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +115 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +516 -0
- package/kanban/server/routes/ceremony.js +305 -0
- package/kanban/server/routes/costs.js +157 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +303 -0
- package/kanban/server/routes/websocket.js +276 -0
- package/kanban/server/routes/work-items.js +347 -0
- package/kanban/server/services/CeremonyService.js +1190 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/sponsor-call-worker.js +84 -0
- package/kanban/server/workers/sprint-planning-worker.js +130 -0
- package/package.json +34 -7
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
3
|
+
import { getAgentList } from '../../lib/api';
|
|
4
|
+
import { AgentEditorPopup } from './AgentEditorPopup';
|
|
5
|
+
|
|
6
|
+
const CEREMONY_STRUCTURE = [
|
|
7
|
+
{
|
|
8
|
+
ceremony: 'Sponsor Call',
|
|
9
|
+
color: 'blue',
|
|
10
|
+
phases: [
|
|
11
|
+
{ phase: 'Mission & Scope', agents: [
|
|
12
|
+
{ slug: 'mission-scope-generator', label: 'Mission Scope Generator', note: 'Generates mission & initial scope' },
|
|
13
|
+
{ slug: 'mission-scope-validator', label: 'Mission Scope Validator', note: 'Validates mission quality' },
|
|
14
|
+
]},
|
|
15
|
+
{ phase: 'Questionnaire', agents: [
|
|
16
|
+
{ slug: 'suggestion-product-manager', label: 'Product Manager', note: 'Fills Initial Scope' },
|
|
17
|
+
{ slug: 'suggestion-ux-researcher', label: 'UX Researcher', note: 'Fills Target Users' },
|
|
18
|
+
{ slug: 'suggestion-deployment-architect', label: 'Deployment Architect', note: 'Fills Deployment Target' },
|
|
19
|
+
{ slug: 'suggestion-technical-architect', label: 'Technical Architect', note: 'Fills Technical Considerations' },
|
|
20
|
+
{ slug: 'suggestion-security-specialist', label: 'Security Specialist', note: 'Fills Security & Compliance' },
|
|
21
|
+
{ slug: 'architecture-recommender', label: 'Architecture Recommender', note: 'Recommends deployment architectures' },
|
|
22
|
+
{ slug: 'database-recommender', label: 'Database Recommender', note: 'Recommends database type' },
|
|
23
|
+
{ slug: 'database-deep-dive', label: 'Database Deep Dive', note: 'Detailed database analysis' },
|
|
24
|
+
{ slug: 'question-prefiller', label: 'Question Prefiller', note: 'Pre-fills answers from architecture' },
|
|
25
|
+
]},
|
|
26
|
+
{ phase: 'Documentation', agents: [
|
|
27
|
+
{ slug: 'project-documentation-creator', label: 'Documentation Creator', note: 'Creates project documentation' },
|
|
28
|
+
{ slug: 'validator-documentation', label: 'Documentation Validator', note: 'Validates documentation quality' },
|
|
29
|
+
]},
|
|
30
|
+
{ phase: 'Context', agents: [
|
|
31
|
+
{ slug: 'migration-guide-generator', label: 'Migration Guide Generator', note: 'Generates cloud migration guide' },
|
|
32
|
+
]},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
ceremony: 'Sprint Planning',
|
|
37
|
+
color: 'purple',
|
|
38
|
+
phases: [
|
|
39
|
+
{ phase: 'Decomposition', agents: [
|
|
40
|
+
{ slug: 'epic-story-decomposer', label: 'Epic Story Decomposer', note: 'Breaks scope into epics & stories' },
|
|
41
|
+
]},
|
|
42
|
+
{ phase: 'Contextual Selection', agents: [
|
|
43
|
+
{ slug: 'project-context-extractor', label: 'Project Context Extractor', note: 'Extracts project traits (once per run) to inform validator selection' },
|
|
44
|
+
{ slug: 'agent-selector', label: 'Agent Selector', note: 'Selects relevant validators per Epic/Story based on project context' },
|
|
45
|
+
]},
|
|
46
|
+
{ phase: 'Documentation & Enrichment', agents: [
|
|
47
|
+
{ slug: 'doc-distributor', label: 'Doc Distributor', note: 'Moves content from parent doc into child docs (project→epic, epic→story)' },
|
|
48
|
+
{ slug: 'story-doc-enricher', label: 'Story Doc Enricher', note: 'Enriches story docs with API contracts, error tables, DB fields, business rules' },
|
|
49
|
+
]},
|
|
50
|
+
{ phase: 'Validation — Epic', agents: [
|
|
51
|
+
{ slug: 'validator-selector', label: 'Validator Selector', note: 'Selects appropriate domain validators' },
|
|
52
|
+
{ slug: 'validator-epic-solution-architect', label: 'Solution Architect' },
|
|
53
|
+
{ slug: 'validator-epic-developer', label: 'Developer' },
|
|
54
|
+
{ slug: 'validator-epic-security', label: 'Security' },
|
|
55
|
+
{ slug: 'validator-epic-devops', label: 'DevOps' },
|
|
56
|
+
{ slug: 'validator-epic-cloud', label: 'Cloud' },
|
|
57
|
+
{ slug: 'validator-epic-backend', label: 'Backend' },
|
|
58
|
+
{ slug: 'validator-epic-database', label: 'Database' },
|
|
59
|
+
{ slug: 'validator-epic-api', label: 'API' },
|
|
60
|
+
{ slug: 'validator-epic-frontend', label: 'Frontend' },
|
|
61
|
+
{ slug: 'validator-epic-ui', label: 'UI' },
|
|
62
|
+
{ slug: 'validator-epic-ux', label: 'UX' },
|
|
63
|
+
{ slug: 'validator-epic-mobile', label: 'Mobile' },
|
|
64
|
+
{ slug: 'validator-epic-data', label: 'Data' },
|
|
65
|
+
{ slug: 'validator-epic-qa', label: 'QA' },
|
|
66
|
+
{ slug: 'validator-epic-test-architect', label: 'Test Architect' },
|
|
67
|
+
]},
|
|
68
|
+
{ phase: 'Solving — Epic', agents: [
|
|
69
|
+
{ slug: 'solver-epic-solution-architect', label: 'Solution Architect' },
|
|
70
|
+
{ slug: 'solver-epic-developer', label: 'Developer' },
|
|
71
|
+
{ slug: 'solver-epic-security', label: 'Security' },
|
|
72
|
+
{ slug: 'solver-epic-devops', label: 'DevOps' },
|
|
73
|
+
{ slug: 'solver-epic-cloud', label: 'Cloud' },
|
|
74
|
+
{ slug: 'solver-epic-backend', label: 'Backend' },
|
|
75
|
+
{ slug: 'solver-epic-database', label: 'Database' },
|
|
76
|
+
{ slug: 'solver-epic-api', label: 'API' },
|
|
77
|
+
{ slug: 'solver-epic-frontend', label: 'Frontend' },
|
|
78
|
+
{ slug: 'solver-epic-ui', label: 'UI' },
|
|
79
|
+
{ slug: 'solver-epic-ux', label: 'UX' },
|
|
80
|
+
{ slug: 'solver-epic-mobile', label: 'Mobile' },
|
|
81
|
+
{ slug: 'solver-epic-data', label: 'Data' },
|
|
82
|
+
{ slug: 'solver-epic-qa', label: 'QA' },
|
|
83
|
+
{ slug: 'solver-epic-test-architect', label: 'Test Architect' },
|
|
84
|
+
]},
|
|
85
|
+
{ phase: 'Validation — Story', agents: [
|
|
86
|
+
{ slug: 'validator-story-solution-architect', label: 'Solution Architect' },
|
|
87
|
+
{ slug: 'validator-story-developer', label: 'Developer' },
|
|
88
|
+
{ slug: 'validator-story-security', label: 'Security' },
|
|
89
|
+
{ slug: 'validator-story-devops', label: 'DevOps' },
|
|
90
|
+
{ slug: 'validator-story-cloud', label: 'Cloud' },
|
|
91
|
+
{ slug: 'validator-story-backend', label: 'Backend' },
|
|
92
|
+
{ slug: 'validator-story-database', label: 'Database' },
|
|
93
|
+
{ slug: 'validator-story-api', label: 'API' },
|
|
94
|
+
{ slug: 'validator-story-frontend', label: 'Frontend' },
|
|
95
|
+
{ slug: 'validator-story-ui', label: 'UI' },
|
|
96
|
+
{ slug: 'validator-story-ux', label: 'UX' },
|
|
97
|
+
{ slug: 'validator-story-mobile', label: 'Mobile' },
|
|
98
|
+
{ slug: 'validator-story-data', label: 'Data' },
|
|
99
|
+
{ slug: 'validator-story-qa', label: 'QA' },
|
|
100
|
+
{ slug: 'validator-story-test-architect', label: 'Test Architect' },
|
|
101
|
+
]},
|
|
102
|
+
{ phase: 'Solving — Story', agents: [
|
|
103
|
+
{ slug: 'solver-story-solution-architect', label: 'Solution Architect' },
|
|
104
|
+
{ slug: 'solver-story-developer', label: 'Developer' },
|
|
105
|
+
{ slug: 'solver-story-security', label: 'Security' },
|
|
106
|
+
{ slug: 'solver-story-devops', label: 'DevOps' },
|
|
107
|
+
{ slug: 'solver-story-cloud', label: 'Cloud' },
|
|
108
|
+
{ slug: 'solver-story-backend', label: 'Backend' },
|
|
109
|
+
{ slug: 'solver-story-database', label: 'Database' },
|
|
110
|
+
{ slug: 'solver-story-api', label: 'API' },
|
|
111
|
+
{ slug: 'solver-story-frontend', label: 'Frontend' },
|
|
112
|
+
{ slug: 'solver-story-ui', label: 'UI' },
|
|
113
|
+
{ slug: 'solver-story-ux', label: 'UX' },
|
|
114
|
+
{ slug: 'solver-story-mobile', label: 'Mobile' },
|
|
115
|
+
{ slug: 'solver-story-data', label: 'Data' },
|
|
116
|
+
{ slug: 'solver-story-qa', label: 'QA' },
|
|
117
|
+
{ slug: 'solver-story-test-architect', label: 'Test Architect' },
|
|
118
|
+
]},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
ceremony: 'Seed',
|
|
123
|
+
color: 'amber',
|
|
124
|
+
phases: [
|
|
125
|
+
{ phase: 'Decomposition', agents: [
|
|
126
|
+
{ slug: 'task-subtask-decomposer', label: 'Task Decomposer', note: 'Breaks stories into tasks & subtasks' },
|
|
127
|
+
]},
|
|
128
|
+
{ phase: 'Documentation', agents: [
|
|
129
|
+
{ slug: 'doc-distributor', label: 'Doc Distributor', note: 'Moves content from story doc into task/subtask docs' },
|
|
130
|
+
{ slug: 'feature-context-generator', label: 'Feature Context Generator', note: 'Generates implementation context.md for each task/subtask' },
|
|
131
|
+
]},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const CEREMONY_COLORS = {
|
|
137
|
+
blue: { border: 'border-blue-200', header: 'bg-blue-50', accent: 'border-l-blue-400', text: 'text-blue-800' },
|
|
138
|
+
purple: { border: 'border-purple-200', header: 'bg-purple-50', accent: 'border-l-purple-400', text: 'text-purple-800' },
|
|
139
|
+
amber: { border: 'border-amber-200', header: 'bg-amber-50', accent: 'border-l-amber-400', text: 'text-amber-800' },
|
|
140
|
+
green: { border: 'border-green-200', header: 'bg-green-50', accent: 'border-l-green-400', text: 'text-green-800' },
|
|
141
|
+
slate: { border: 'border-slate-200', header: 'bg-slate-50', accent: 'border-l-slate-400', text: 'text-slate-800' },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Flat set of all known slugs for computing "Other" group
|
|
145
|
+
const KNOWN_SLUGS = new Set(
|
|
146
|
+
CEREMONY_STRUCTURE.flatMap(c => c.phases.flatMap(p => p.agents.map(a => a.slug)))
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
export function AgentsTab() {
|
|
150
|
+
const [agentStatus, setAgentStatus] = useState({}); // { slug: isCustomized }
|
|
151
|
+
const [openAgent, setOpenAgent] = useState(null); // slug | null
|
|
152
|
+
const [search, setSearch] = useState('');
|
|
153
|
+
const [error, setError] = useState(null);
|
|
154
|
+
// collapsed state: absence = collapsed (default), true = open
|
|
155
|
+
const [collapsed, setCollapsed] = useState({});
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
getAgentList()
|
|
159
|
+
.then(r => {
|
|
160
|
+
const status = {};
|
|
161
|
+
r.agents.forEach(a => {
|
|
162
|
+
const slug = a.name.replace(/\.md$/, '');
|
|
163
|
+
status[slug] = a.isCustomized;
|
|
164
|
+
});
|
|
165
|
+
setAgentStatus(status);
|
|
166
|
+
})
|
|
167
|
+
.catch(err => setError(err.message));
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const toggle = (key) => setCollapsed(prev => ({ ...prev, [key]: !prev[key] }));
|
|
171
|
+
const isOpen = (key) => collapsed[key] === true; // default collapsed when key absent
|
|
172
|
+
|
|
173
|
+
// Build "Other" group from agents returned by the API that aren't in any ceremony structure
|
|
174
|
+
const otherAgents = Object.keys(agentStatus)
|
|
175
|
+
.filter(slug => !KNOWN_SLUGS.has(slug))
|
|
176
|
+
.map(slug => ({ slug, label: slug, note: null }));
|
|
177
|
+
|
|
178
|
+
const allCeremonies = [
|
|
179
|
+
...CEREMONY_STRUCTURE,
|
|
180
|
+
...(otherAgents.length > 0
|
|
181
|
+
? [{ ceremony: 'Other', color: 'slate', phases: [{ phase: 'Other', agents: otherAgents }] }]
|
|
182
|
+
: []),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
// Filter by search query — when searching, force everything open
|
|
186
|
+
const q = search.toLowerCase();
|
|
187
|
+
const filteredCeremonies = q
|
|
188
|
+
? allCeremonies
|
|
189
|
+
.map(c => ({
|
|
190
|
+
...c,
|
|
191
|
+
phases: c.phases
|
|
192
|
+
.map(p => ({
|
|
193
|
+
...p,
|
|
194
|
+
agents: p.agents.filter(a =>
|
|
195
|
+
a.label.toLowerCase().includes(q) ||
|
|
196
|
+
a.slug.toLowerCase().includes(q) ||
|
|
197
|
+
c.ceremony.toLowerCase().includes(q) ||
|
|
198
|
+
p.phase.toLowerCase().includes(q)
|
|
199
|
+
),
|
|
200
|
+
}))
|
|
201
|
+
.filter(p => p.agents.length > 0),
|
|
202
|
+
}))
|
|
203
|
+
.filter(c => c.phases.length > 0)
|
|
204
|
+
: allCeremonies;
|
|
205
|
+
|
|
206
|
+
const hasAgentsLoaded = Object.keys(agentStatus).length > 0;
|
|
207
|
+
const forceOpen = q.length > 0;
|
|
208
|
+
|
|
209
|
+
// Count customized agents per ceremony for the badge
|
|
210
|
+
const countCustomized = (ceremony) =>
|
|
211
|
+
ceremony.phases
|
|
212
|
+
.flatMap(p => p.agents)
|
|
213
|
+
.filter(a => agentStatus[a.slug])
|
|
214
|
+
.length;
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div>
|
|
218
|
+
{/* Sticky search bar */}
|
|
219
|
+
<div className="sticky top-0 bg-white z-10 px-4 py-2.5 border-b border-slate-100 flex items-center justify-between gap-3">
|
|
220
|
+
<input
|
|
221
|
+
type="search"
|
|
222
|
+
placeholder="Search agents…"
|
|
223
|
+
value={search}
|
|
224
|
+
onChange={e => setSearch(e.target.value)}
|
|
225
|
+
className="w-48 rounded border border-slate-200 px-3 py-1.5 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
226
|
+
/>
|
|
227
|
+
<span className="text-xs text-slate-400 italic flex-shrink-0">Click any agent to edit its prompt</span>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* Agent hierarchy list */}
|
|
231
|
+
<div className="px-4 py-4 flex flex-col gap-3">
|
|
232
|
+
{error && (
|
|
233
|
+
<p className="text-xs text-red-500">{error}</p>
|
|
234
|
+
)}
|
|
235
|
+
{filteredCeremonies.length === 0 && (
|
|
236
|
+
<p className="text-sm text-slate-400 py-4">No agents match your search.</p>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{filteredCeremonies.map(ceremony => {
|
|
240
|
+
const colors = CEREMONY_COLORS[ceremony.color] || CEREMONY_COLORS.slate;
|
|
241
|
+
const ceremonyOpen = forceOpen || isOpen(ceremony.ceremony);
|
|
242
|
+
const customCount = countCustomized(ceremony);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div
|
|
246
|
+
key={ceremony.ceremony}
|
|
247
|
+
className={`rounded-xl border ${colors.border} overflow-hidden`}
|
|
248
|
+
>
|
|
249
|
+
{/* Ceremony header — clickable to collapse/expand */}
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
onClick={() => !forceOpen && toggle(ceremony.ceremony)}
|
|
253
|
+
className={`w-full flex items-center gap-2 px-4 py-2.5 border-l-4 ${colors.header} ${colors.accent} text-left`}
|
|
254
|
+
>
|
|
255
|
+
{ceremonyOpen
|
|
256
|
+
? <ChevronDown className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
|
257
|
+
: <ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
|
258
|
+
}
|
|
259
|
+
<span className={`text-sm font-semibold flex-1 ${colors.text}`}>
|
|
260
|
+
{ceremony.ceremony}
|
|
261
|
+
</span>
|
|
262
|
+
{customCount > 0 && (
|
|
263
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
264
|
+
{customCount} custom
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
267
|
+
</button>
|
|
268
|
+
|
|
269
|
+
{/* Ceremony body — collapsible */}
|
|
270
|
+
{ceremonyOpen && (
|
|
271
|
+
<div className="divide-y divide-slate-100">
|
|
272
|
+
{ceremony.phases.map(phase => {
|
|
273
|
+
const phaseKey = `${ceremony.ceremony}::${phase.phase}`;
|
|
274
|
+
const phaseOpen = forceOpen || isOpen(phaseKey);
|
|
275
|
+
const visibleAgents = phase.agents.filter(
|
|
276
|
+
a => !hasAgentsLoaded || a.slug in agentStatus
|
|
277
|
+
);
|
|
278
|
+
if (visibleAgents.length === 0) return null;
|
|
279
|
+
|
|
280
|
+
return (
|
|
281
|
+
<div key={phase.phase}>
|
|
282
|
+
{/* Phase sub-header — indented, highlighted as non-leaf node */}
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => !forceOpen && toggle(phaseKey)}
|
|
286
|
+
className="w-full flex items-center gap-2 pl-6 pr-4 py-2 bg-slate-50 hover:bg-slate-100 transition-colors text-left"
|
|
287
|
+
>
|
|
288
|
+
{phaseOpen
|
|
289
|
+
? <ChevronDown className="w-3 h-3 text-slate-400 flex-shrink-0" />
|
|
290
|
+
: <ChevronRight className="w-3 h-3 text-slate-400 flex-shrink-0" />
|
|
291
|
+
}
|
|
292
|
+
<span className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
|
|
293
|
+
{phase.phase}
|
|
294
|
+
</span>
|
|
295
|
+
<span className="text-xs text-slate-400 ml-1">
|
|
296
|
+
{visibleAgents.length}
|
|
297
|
+
</span>
|
|
298
|
+
</button>
|
|
299
|
+
|
|
300
|
+
{/* Agent rows — further indented */}
|
|
301
|
+
{phaseOpen && (
|
|
302
|
+
<div className="pb-1">
|
|
303
|
+
{visibleAgents.map(agent => {
|
|
304
|
+
const isCustomized = agentStatus[agent.slug] ?? false;
|
|
305
|
+
return (
|
|
306
|
+
<button
|
|
307
|
+
key={`${ceremony.ceremony}-${agent.slug}`}
|
|
308
|
+
type="button"
|
|
309
|
+
onClick={() => setOpenAgent(agent.slug)}
|
|
310
|
+
className="w-full text-left pl-10 pr-4 py-1.5 hover:bg-slate-50 transition-colors flex items-center gap-3 group"
|
|
311
|
+
>
|
|
312
|
+
<div className="flex-1 min-w-0">
|
|
313
|
+
<span className="text-sm text-slate-700 group-hover:text-slate-900">
|
|
314
|
+
{agent.label}
|
|
315
|
+
</span>
|
|
316
|
+
{agent.note && (
|
|
317
|
+
<span className="text-xs text-slate-400 italic ml-2">
|
|
318
|
+
{agent.note}
|
|
319
|
+
</span>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
{isCustomized && (
|
|
323
|
+
<span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
|
|
324
|
+
Custom
|
|
325
|
+
</span>
|
|
326
|
+
)}
|
|
327
|
+
</button>
|
|
328
|
+
);
|
|
329
|
+
})}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
})}
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
})}
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
{/* Agent editor popup */}
|
|
343
|
+
{openAgent && (
|
|
344
|
+
<AgentEditorPopup
|
|
345
|
+
agentName={`${openAgent}.md`}
|
|
346
|
+
onClose={() => setOpenAgent(null)}
|
|
347
|
+
onSaved={() => setAgentStatus(prev => ({ ...prev, [openAgent]: true }))}
|
|
348
|
+
onReset={() => setAgentStatus(prev => ({ ...prev, [openAgent]: false }))}
|
|
349
|
+
/>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Eye, EyeOff } from 'lucide-react';
|
|
3
|
+
import { saveApiKeys } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
const PROVIDERS = [
|
|
6
|
+
{ key: 'anthropic', label: 'Anthropic', envKey: 'ANTHROPIC_API_KEY', placeholder: 'sk-ant-…' },
|
|
7
|
+
{ key: 'gemini', label: 'Google (Gemini)', envKey: 'GEMINI_API_KEY', placeholder: 'AIza…' },
|
|
8
|
+
{ key: 'openai', label: 'OpenAI', envKey: 'OPENAI_API_KEY', placeholder: 'sk-…' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function ApiKeyRow({ provider, apiKeyInfo, onSaved }) {
|
|
12
|
+
const [value, setValue] = useState('');
|
|
13
|
+
const [showKey, setShowKey] = useState(false);
|
|
14
|
+
const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
|
|
15
|
+
|
|
16
|
+
const handleSave = async () => {
|
|
17
|
+
setStatus('saving');
|
|
18
|
+
try {
|
|
19
|
+
await saveApiKeys({ [provider.key]: value });
|
|
20
|
+
setStatus('saved');
|
|
21
|
+
setValue('');
|
|
22
|
+
onSaved();
|
|
23
|
+
setTimeout(() => setStatus(null), 2000);
|
|
24
|
+
} catch {
|
|
25
|
+
setStatus('error');
|
|
26
|
+
setTimeout(() => setStatus(null), 2000);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex items-center gap-3 py-3 border-b border-slate-100 last:border-0">
|
|
32
|
+
{/* Provider name */}
|
|
33
|
+
<div className="w-36 flex-shrink-0">
|
|
34
|
+
<p className="text-sm font-medium text-slate-800">{provider.label}</p>
|
|
35
|
+
<p className="text-xs text-slate-400">{provider.envKey}</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Status badge */}
|
|
39
|
+
<div className="w-16 flex-shrink-0">
|
|
40
|
+
{apiKeyInfo.isSet ? (
|
|
41
|
+
<span className="inline-flex items-center gap-1 text-xs font-medium text-green-700 bg-green-50 border border-green-200 rounded-full px-2 py-0.5">
|
|
42
|
+
✓ Set
|
|
43
|
+
</span>
|
|
44
|
+
) : (
|
|
45
|
+
<span className="inline-flex items-center text-xs font-medium text-slate-400 bg-slate-50 border border-slate-200 rounded-full px-2 py-0.5">
|
|
46
|
+
Not set
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Preview */}
|
|
52
|
+
{apiKeyInfo.isSet && !value && (
|
|
53
|
+
<p className="text-xs text-slate-400 font-mono flex-shrink-0">{apiKeyInfo.preview}</p>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{/* Key input */}
|
|
57
|
+
<div className="flex-1 flex items-center gap-2 min-w-0">
|
|
58
|
+
<div className="relative flex-1">
|
|
59
|
+
<input
|
|
60
|
+
type={showKey ? 'text' : 'password'}
|
|
61
|
+
value={value}
|
|
62
|
+
onChange={(e) => setValue(e.target.value)}
|
|
63
|
+
placeholder={apiKeyInfo.isSet ? 'Enter new key to update…' : provider.placeholder}
|
|
64
|
+
className="w-full rounded-md border border-slate-300 px-2 py-1.5 pr-8 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
|
|
65
|
+
/>
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
onClick={() => setShowKey((v) => !v)}
|
|
69
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
|
|
70
|
+
tabIndex={-1}
|
|
71
|
+
>
|
|
72
|
+
{showKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={handleSave}
|
|
79
|
+
disabled={!value.trim() || status === 'saving'}
|
|
80
|
+
className="px-3 py-1.5 text-xs font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 flex-shrink-0"
|
|
81
|
+
>
|
|
82
|
+
{status === 'saving' ? (
|
|
83
|
+
<span className="inline-flex items-center gap-1">
|
|
84
|
+
<span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
85
|
+
Saving
|
|
86
|
+
</span>
|
|
87
|
+
) : status === 'saved' ? '✓ Saved' : status === 'error' ? '✗ Error' : 'Save'}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function ApiKeysTab({ settings, onSaved }) {
|
|
95
|
+
return (
|
|
96
|
+
<div className="px-5 py-4">
|
|
97
|
+
<p className="text-xs text-slate-500 mb-4">
|
|
98
|
+
API keys are stored in your project's <code className="font-mono bg-slate-100 px-1 rounded">.env</code> file.
|
|
99
|
+
Enter a new key and click Save to update. Clear the field and save to remove a key.
|
|
100
|
+
</p>
|
|
101
|
+
<div>
|
|
102
|
+
{PROVIDERS.map((provider) => (
|
|
103
|
+
<ApiKeyRow
|
|
104
|
+
key={provider.key}
|
|
105
|
+
provider={provider}
|
|
106
|
+
apiKeyInfo={settings.apiKeys[provider.key]}
|
|
107
|
+
onSaved={onSaved}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Workflow } from 'lucide-react';
|
|
3
|
+
import { saveCeremonies } from '../../lib/api';
|
|
4
|
+
import { CeremonyWorkflowModal } from '../ceremony/CeremonyWorkflowModal';
|
|
5
|
+
|
|
6
|
+
function humanize(str) {
|
|
7
|
+
return str
|
|
8
|
+
.replace(/-/g, ' ')
|
|
9
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CEREMONY_DESCRIPTIONS = {
|
|
13
|
+
'sponsor-call': 'Generates mission statement and project documentation through a structured AI-guided interview.',
|
|
14
|
+
'sprint-planning': 'Plans and assigns work items for the upcoming sprint based on team capacity and priorities.',
|
|
15
|
+
'seed': 'Seeds the initial project work item structure from the project documentation.',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function CeremonyModelsTab({ settings, models, onSaved }) {
|
|
19
|
+
const [ceremonies, setCeremonies] = useState(
|
|
20
|
+
() => JSON.parse(JSON.stringify(settings.ceremonies || []))
|
|
21
|
+
);
|
|
22
|
+
const [missionGenValidation, setMissionGenValidation] = useState(
|
|
23
|
+
() => JSON.parse(JSON.stringify(
|
|
24
|
+
settings.missionGenerator?.validation || { maxIterations: 3, acceptanceThreshold: 95 }
|
|
25
|
+
))
|
|
26
|
+
);
|
|
27
|
+
const [activeWorkflow, setActiveWorkflow] = useState(null);
|
|
28
|
+
|
|
29
|
+
const handleCeremonySave = async (updatedCeremony, updatedMG) => {
|
|
30
|
+
const next = ceremonies.map((c) =>
|
|
31
|
+
c.name === updatedCeremony.name ? updatedCeremony : c
|
|
32
|
+
);
|
|
33
|
+
// Always pass missionGen params; use updated value for sponsor-call, current for others
|
|
34
|
+
const missionGenArg = updatedCeremony.name === 'sponsor-call'
|
|
35
|
+
? { validation: updatedMG || missionGenValidation }
|
|
36
|
+
: { validation: missionGenValidation };
|
|
37
|
+
await saveCeremonies(next, missionGenArg);
|
|
38
|
+
setCeremonies(next);
|
|
39
|
+
if (updatedCeremony.name === 'sponsor-call' && updatedMG) {
|
|
40
|
+
setMissionGenValidation(updatedMG);
|
|
41
|
+
}
|
|
42
|
+
onSaved();
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (!ceremonies.length) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="px-5 py-8 text-center">
|
|
48
|
+
<p className="text-sm text-slate-500">
|
|
49
|
+
No ceremony configurations found yet. Run your first ceremony from the kanban board
|
|
50
|
+
to populate settings here.
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const activeWorkflowCeremony = ceremonies.find((c) => c.name === activeWorkflow);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="px-5 py-4 flex flex-col gap-3">
|
|
60
|
+
{ceremonies.map((ceremony) => {
|
|
61
|
+
const description = CEREMONY_DESCRIPTIONS[ceremony.name];
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
key={ceremony.name}
|
|
65
|
+
className="border border-slate-200 rounded-lg px-4 py-3 flex items-center justify-between gap-4"
|
|
66
|
+
>
|
|
67
|
+
<div className="min-w-0">
|
|
68
|
+
<p className="text-sm font-semibold text-slate-800">
|
|
69
|
+
{ceremony.displayName || humanize(ceremony.name || '')}
|
|
70
|
+
</p>
|
|
71
|
+
{description && (
|
|
72
|
+
<p className="text-xs text-slate-500 mt-0.5 leading-relaxed">{description}</p>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={() => setActiveWorkflow(ceremony.name)}
|
|
78
|
+
className="flex items-center gap-1.5 text-xs font-medium text-blue-600 hover:text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded-md px-3 py-1.5 transition-colors flex-shrink-0"
|
|
79
|
+
>
|
|
80
|
+
<Workflow className="w-3.5 h-3.5" />
|
|
81
|
+
Configure Models
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
|
|
87
|
+
{activeWorkflow && activeWorkflowCeremony && (
|
|
88
|
+
<CeremonyWorkflowModal
|
|
89
|
+
ceremony={activeWorkflowCeremony}
|
|
90
|
+
models={models}
|
|
91
|
+
missionGenValidation={activeWorkflow === 'sponsor-call' ? missionGenValidation : null}
|
|
92
|
+
onClose={() => setActiveWorkflow(null)}
|
|
93
|
+
onSave={handleCeremonySave}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { saveCostThresholds } from '../../lib/api';
|
|
3
|
+
|
|
4
|
+
const CEREMONIES = [
|
|
5
|
+
{ key: 'sponsor-call', label: 'Sponsor Call', desc: 'Wizard to define project mission, scope, and architecture' },
|
|
6
|
+
{ key: 'sprint-planning', label: 'Sprint Planning', desc: 'Generates epics, stories, and feature contexts' },
|
|
7
|
+
{ key: 'seed', label: 'Seed', desc: 'Populates initial epics and stories from a seed document' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function initState(costThresholds) {
|
|
11
|
+
const state = {};
|
|
12
|
+
for (const { key } of CEREMONIES) {
|
|
13
|
+
const val = costThresholds?.[key];
|
|
14
|
+
state[key] = val != null ? String(val) : '';
|
|
15
|
+
}
|
|
16
|
+
return state;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CostThresholdsTab({ settings, onSaved }) {
|
|
20
|
+
const [values, setValues] = useState(() => initState(settings.costThresholds));
|
|
21
|
+
const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
|
|
22
|
+
|
|
23
|
+
const update = (key, val) => {
|
|
24
|
+
setValues((prev) => ({ ...prev, [key]: val }));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const handleSave = async () => {
|
|
28
|
+
setStatus('saving');
|
|
29
|
+
try {
|
|
30
|
+
const payload = {};
|
|
31
|
+
for (const { key } of CEREMONIES) {
|
|
32
|
+
const raw = values[key].trim();
|
|
33
|
+
payload[key] = raw === '' ? null : parseFloat(raw) || null;
|
|
34
|
+
}
|
|
35
|
+
await saveCostThresholds(payload);
|
|
36
|
+
setStatus('saved');
|
|
37
|
+
onSaved();
|
|
38
|
+
setTimeout(() => setStatus(null), 2000);
|
|
39
|
+
} catch {
|
|
40
|
+
setStatus('error');
|
|
41
|
+
setTimeout(() => setStatus(null), 2000);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="px-5 py-4 flex flex-col gap-4">
|
|
47
|
+
<p className="text-xs text-slate-500">
|
|
48
|
+
Set a maximum spend (in <strong>USD</strong>) per ceremony run. Leave empty for unlimited.
|
|
49
|
+
When the running cost exceeds the limit, the ceremony stops automatically.
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<div className="flex flex-col gap-2">
|
|
53
|
+
{CEREMONIES.map(({ key, label, desc }) => (
|
|
54
|
+
<div key={key} className="border border-slate-200 rounded-lg p-4">
|
|
55
|
+
<div className="flex items-start justify-between gap-4">
|
|
56
|
+
<div className="flex-1 min-w-0">
|
|
57
|
+
<div className="text-sm font-semibold text-slate-800">{label}</div>
|
|
58
|
+
<div className="text-xs text-slate-500 mt-0.5">{desc}</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
61
|
+
<span className="text-sm text-slate-500">$</span>
|
|
62
|
+
<input
|
|
63
|
+
type="number"
|
|
64
|
+
min="0"
|
|
65
|
+
step="0.01"
|
|
66
|
+
value={values[key]}
|
|
67
|
+
onChange={(e) => update(key, e.target.value)}
|
|
68
|
+
placeholder="Unlimited"
|
|
69
|
+
className="rounded-md border border-slate-300 px-2 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-28 placeholder:text-slate-400"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="flex justify-end pt-1">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={handleSave}
|
|
81
|
+
disabled={status === 'saving'}
|
|
82
|
+
className="px-4 py-2 text-sm font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40"
|
|
83
|
+
>
|
|
84
|
+
{status === 'saving' ? (
|
|
85
|
+
<span className="inline-flex items-center gap-2">
|
|
86
|
+
<span className="w-3.5 h-3.5 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
87
|
+
Saving…
|
|
88
|
+
</span>
|
|
89
|
+
) : status === 'saved' ? '✓ Saved' : status === 'error' ? '✗ Error' : 'Save Cost Limits'}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|