@agile-vibe-coding/avc 0.1.1 → 0.3.1
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/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -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/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -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 +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -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/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -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 +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -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/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -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 +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -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-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -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 +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -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 +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -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 +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -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 +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -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 +384 -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 +177 -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 +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -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 +123 -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 +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -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/TaskRunnerService.js +261 -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/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { X, Pencil } from 'lucide-react';
|
|
3
|
+
import { AgentEditorPopup } from '../settings/AgentEditorPopup';
|
|
4
|
+
import { CheckEditorPopup } from '../settings/CheckEditorPopup';
|
|
5
|
+
import { ProviderSwitcherButton } from './ProviderSwitcherButton';
|
|
6
|
+
|
|
7
|
+
// Human-readable labels for agent slugs
|
|
8
|
+
const AGENT_LABELS = {
|
|
9
|
+
// Sponsor Call
|
|
10
|
+
'mission-scope-generator': 'Mission Scope Generator',
|
|
11
|
+
'mission-scope-validator': 'Mission Scope Validator',
|
|
12
|
+
'suggestion-ux-researcher': 'UX Researcher',
|
|
13
|
+
'suggestion-product-manager': 'Product Manager',
|
|
14
|
+
'suggestion-deployment-architect': 'Deployment Architect',
|
|
15
|
+
'suggestion-technical-architect': 'Technical Architect',
|
|
16
|
+
'suggestion-security-specialist': 'Security Specialist',
|
|
17
|
+
'architecture-recommender': 'Architecture Recommender',
|
|
18
|
+
'question-prefiller': 'Question Prefiller',
|
|
19
|
+
'project-documentation-creator': 'Documentation Creator',
|
|
20
|
+
'validator-documentation': 'Documentation Validator',
|
|
21
|
+
// Sprint Planning + Seed (shared)
|
|
22
|
+
'doc-distributor': 'Doc Distributor',
|
|
23
|
+
'feature-context-generator': 'Feature Context Generator',
|
|
24
|
+
// Sprint Planning
|
|
25
|
+
'epic-story-decomposer': 'Epic/Story Decomposer',
|
|
26
|
+
'story-scope-reviewer': 'Story Scope Reviewer',
|
|
27
|
+
'project-context-extractor': 'Project Context Extractor',
|
|
28
|
+
'agent-selector': 'Agent Selector',
|
|
29
|
+
'context-writer-epic': 'Context Writer (Epic)',
|
|
30
|
+
'context-reviewer-epic': 'Context Reviewer (Epic)',
|
|
31
|
+
'context-writer-story': 'Context Writer (Story)',
|
|
32
|
+
'context-reviewer-story': 'Context Reviewer (Story)',
|
|
33
|
+
'doc-writer-epic': 'Doc Writer (Epic)',
|
|
34
|
+
'doc-writer-story': 'Doc Writer (Story)',
|
|
35
|
+
'story-doc-enricher': 'Story Doc Enricher',
|
|
36
|
+
'validator-selector': 'Validator Selector',
|
|
37
|
+
'validator-epic-solution-architect': 'Solution Architect (Epic)',
|
|
38
|
+
'validator-epic-developer': 'Developer (Epic)',
|
|
39
|
+
'validator-epic-security': 'Security (Epic)',
|
|
40
|
+
'validator-epic-backend': 'Backend (Epic)',
|
|
41
|
+
'validator-epic-frontend': 'Frontend (Epic)',
|
|
42
|
+
'validator-epic-ux': 'UX (Epic)',
|
|
43
|
+
'validator-story-developer': 'Developer (Story)',
|
|
44
|
+
'validator-story-qa': 'QA (Story)',
|
|
45
|
+
'validator-story-test-architect': 'Test Architect (Story)',
|
|
46
|
+
'validator-story-solution-architect': 'Solution Architect (Story)',
|
|
47
|
+
'validator-story-security': 'Security (Story)',
|
|
48
|
+
'validator-story-backend': 'Backend (Story)',
|
|
49
|
+
'validator-story-frontend': 'Frontend (Story)',
|
|
50
|
+
'validator-story-ux': 'UX (Story)',
|
|
51
|
+
'story-splitter': 'Story Splitter',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ── Domain check perspectives (matching src/cli/checks/{epic|story}/*.json) ──
|
|
55
|
+
|
|
56
|
+
const DOMAIN_CHECK_PERSPECTIVES = [
|
|
57
|
+
'solution-architect', 'developer', 'security', 'devops', 'cloud',
|
|
58
|
+
'backend', 'database', 'api', 'frontend', 'ui', 'ux',
|
|
59
|
+
'mobile', 'data', 'qa', 'test-architect',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// ── Step type config ──────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const STEP_TYPE_CONFIG = {
|
|
65
|
+
generate: { label: 'Generate', cls: 'bg-blue-50 text-blue-700 border-blue-200' },
|
|
66
|
+
validate: { label: 'Validate', cls: 'bg-amber-50 text-amber-700 border-amber-200' },
|
|
67
|
+
refine: { label: 'Refine', cls: 'bg-orange-50 text-orange-700 border-orange-200' },
|
|
68
|
+
input: { label: 'User Input', cls: 'bg-purple-50 text-purple-700 border-purple-200' },
|
|
69
|
+
cross: { label: 'Cross-validate', cls: 'bg-indigo-50 text-indigo-700 border-indigo-200' },
|
|
70
|
+
output: { label: 'Write', cls: 'bg-emerald-50 text-emerald-700 border-emerald-200' },
|
|
71
|
+
read: { label: 'Read', cls: 'bg-sky-50 text-sky-700 border-sky-200' },
|
|
72
|
+
process: { label: 'Process', cls: 'bg-slate-50 text-slate-600 border-slate-200' },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const PHASE_COLOR_CONFIG = {
|
|
76
|
+
blue: { dot: 'bg-blue-500', label: 'text-blue-600' },
|
|
77
|
+
purple: { dot: 'bg-purple-500', label: 'text-purple-600' },
|
|
78
|
+
amber: { dot: 'bg-amber-500', label: 'text-amber-600' },
|
|
79
|
+
green: { dot: 'bg-green-500', label: 'text-green-600' },
|
|
80
|
+
emerald: { dot: 'bg-emerald-500', label: 'text-emerald-600' },
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Loop group color palettes
|
|
84
|
+
const LOOP_C = {
|
|
85
|
+
amber: {
|
|
86
|
+
border: 'border-amber-400',
|
|
87
|
+
bg: 'bg-amber-50',
|
|
88
|
+
hdr: 'bg-amber-100',
|
|
89
|
+
hdrBorder: 'border-amber-300',
|
|
90
|
+
text: 'text-amber-800',
|
|
91
|
+
subtext: 'text-amber-600',
|
|
92
|
+
line: 'bg-amber-400',
|
|
93
|
+
chip: 'bg-amber-200 text-amber-900',
|
|
94
|
+
arrow: 'text-amber-500',
|
|
95
|
+
condLine: 'bg-amber-300',
|
|
96
|
+
},
|
|
97
|
+
indigo: {
|
|
98
|
+
border: 'border-indigo-400',
|
|
99
|
+
bg: 'bg-indigo-50',
|
|
100
|
+
hdr: 'bg-indigo-100',
|
|
101
|
+
hdrBorder: 'border-indigo-300',
|
|
102
|
+
text: 'text-indigo-800',
|
|
103
|
+
subtext: 'text-indigo-600',
|
|
104
|
+
line: 'bg-indigo-400',
|
|
105
|
+
chip: 'bg-indigo-200 text-indigo-900',
|
|
106
|
+
arrow: 'text-indigo-500',
|
|
107
|
+
condLine: 'bg-indigo-300',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ── Phase builders ────────────────────────────────────────────────────────────
|
|
112
|
+
//
|
|
113
|
+
// Step metadata fields (in addition to type / label / agent / agents):
|
|
114
|
+
// stageKey — key in ceremony.stages to read/write the model
|
|
115
|
+
// validationKey — 'top' (ceremony.validation.model) or area name ('documentation', 'context')
|
|
116
|
+
// sharedWith — string shown instead of a select; marks a secondary reference to the same key
|
|
117
|
+
// loopParamType — 'missionGen' | 'docContext' | 'crossValidation'
|
|
118
|
+
// loopParamReadOnly — true: show values read-only (params already editable in a sibling loop)
|
|
119
|
+
|
|
120
|
+
function buildSponsorCallPhases(ceremony, missionGenValidation) {
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
id: 'mission',
|
|
124
|
+
label: 'Mission & Scope',
|
|
125
|
+
color: 'blue',
|
|
126
|
+
steps: [
|
|
127
|
+
{
|
|
128
|
+
type: 'generate',
|
|
129
|
+
label: 'Generate mission statement & initial scope',
|
|
130
|
+
model: ceremony.stages?.suggestions?.model,
|
|
131
|
+
stageKey: 'suggestions',
|
|
132
|
+
agent: 'mission-scope-generator',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'loop-group',
|
|
136
|
+
loopParamType: 'missionGen',
|
|
137
|
+
loop: {
|
|
138
|
+
max: missionGenValidation?.maxIterations ?? 3,
|
|
139
|
+
threshold: missionGenValidation?.acceptanceThreshold ?? 95,
|
|
140
|
+
},
|
|
141
|
+
steps: [
|
|
142
|
+
{
|
|
143
|
+
type: 'validate',
|
|
144
|
+
label: 'Validate quality against acceptance threshold',
|
|
145
|
+
model: ceremony.validation?.model,
|
|
146
|
+
validationKey: 'top',
|
|
147
|
+
agent: 'mission-scope-validator',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: 'refine',
|
|
151
|
+
label: 'Refine based on validation issues',
|
|
152
|
+
model: ceremony.stages?.suggestions?.model,
|
|
153
|
+
stageKey: 'suggestions',
|
|
154
|
+
sharedWith: 'Mission generator',
|
|
155
|
+
agent: 'mission-scope-generator',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'questionnaire',
|
|
163
|
+
label: 'Questionnaire',
|
|
164
|
+
color: 'purple',
|
|
165
|
+
steps: [
|
|
166
|
+
{
|
|
167
|
+
type: 'input',
|
|
168
|
+
label: '5 project definition questions (user-provided or skipped)',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'generate',
|
|
172
|
+
label: 'Auto-fill any skipped questions',
|
|
173
|
+
model: ceremony.stages?.suggestions?.model,
|
|
174
|
+
stageKey: 'suggestions',
|
|
175
|
+
sharedWith: 'Mission generator',
|
|
176
|
+
agents: [
|
|
177
|
+
{ slug: 'suggestion-product-manager', note: 'fills Initial Scope' },
|
|
178
|
+
{ slug: 'suggestion-ux-researcher', note: 'fills Target Users' },
|
|
179
|
+
{ slug: 'suggestion-deployment-architect', note: 'fills Deployment Target' },
|
|
180
|
+
{ slug: 'suggestion-technical-architect', note: 'fills Technical Considerations' },
|
|
181
|
+
{ slug: 'suggestion-security-specialist', note: 'fills Security & Compliance' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'generate',
|
|
186
|
+
label: 'Architecture recommendation',
|
|
187
|
+
model: ceremony.stages?.['architecture-recommendation']?.model,
|
|
188
|
+
stageKey: 'architecture-recommendation',
|
|
189
|
+
agent: 'architecture-recommender',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
type: 'generate',
|
|
193
|
+
label: 'Pre-fill answers from architecture analysis',
|
|
194
|
+
model: ceremony.stages?.['question-prefilling']?.model,
|
|
195
|
+
stageKey: 'question-prefilling',
|
|
196
|
+
agent: 'question-prefiller',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: 'documentation',
|
|
202
|
+
label: 'Documentation',
|
|
203
|
+
color: 'amber',
|
|
204
|
+
steps: [
|
|
205
|
+
{
|
|
206
|
+
type: 'generate',
|
|
207
|
+
label: 'Generate project documentation (doc.md)',
|
|
208
|
+
model: ceremony.stages?.documentation?.model,
|
|
209
|
+
stageKey: 'documentation',
|
|
210
|
+
agent: 'project-documentation-creator',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: 'loop-group',
|
|
214
|
+
loopParamType: 'docContext',
|
|
215
|
+
loop: {
|
|
216
|
+
max: ceremony.validation?.maxIterations ?? 100,
|
|
217
|
+
threshold: ceremony.validation?.acceptanceThreshold ?? 95,
|
|
218
|
+
},
|
|
219
|
+
steps: [
|
|
220
|
+
{
|
|
221
|
+
type: 'validate',
|
|
222
|
+
label: 'Validate documentation quality',
|
|
223
|
+
model: ceremony.validation?.documentation?.model ?? ceremony.validation?.model,
|
|
224
|
+
validationKey: 'documentation',
|
|
225
|
+
agent: 'validator-documentation',
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
type: 'refine',
|
|
229
|
+
label: 'Improve documentation based on issues',
|
|
230
|
+
model: ceremony.stages?.documentation?.model,
|
|
231
|
+
stageKey: 'documentation',
|
|
232
|
+
sharedWith: 'Documentation generator',
|
|
233
|
+
agent: 'project-documentation-creator',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'output',
|
|
241
|
+
label: 'Output',
|
|
242
|
+
color: 'emerald',
|
|
243
|
+
steps: [
|
|
244
|
+
{ type: 'output', label: '.avc/project/doc.md written', files: [{ name: 'project/doc.md', direction: 'out' }] },
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildSprintPlanningPhases(ceremony) {
|
|
251
|
+
// Stages that aren't explicitly configured fall back to ceremony.defaultModel
|
|
252
|
+
const fallbackModel = ceremony.defaultModel;
|
|
253
|
+
|
|
254
|
+
return [
|
|
255
|
+
{
|
|
256
|
+
id: 'scope',
|
|
257
|
+
label: 'Scope Collection',
|
|
258
|
+
color: 'blue',
|
|
259
|
+
steps: [
|
|
260
|
+
{
|
|
261
|
+
type: 'read',
|
|
262
|
+
label: 'Read project scope',
|
|
263
|
+
files: [{ name: 'project/doc.md', direction: 'in', note: '.avc/project/doc.md — scope section extracted and sent to decomposer' }],
|
|
264
|
+
},
|
|
265
|
+
{ type: 'read', label: 'Analyse existing Epics & Stories (deduplication baseline)' },
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 'decomposition',
|
|
270
|
+
label: 'Decomposition',
|
|
271
|
+
color: 'purple',
|
|
272
|
+
steps: [
|
|
273
|
+
{
|
|
274
|
+
type: 'generate',
|
|
275
|
+
label: 'Decompose scope into Epics and Stories',
|
|
276
|
+
model: ceremony.stages?.decomposition?.model ?? fallbackModel,
|
|
277
|
+
stageKey: 'decomposition',
|
|
278
|
+
agent: 'epic-story-decomposer',
|
|
279
|
+
files: [
|
|
280
|
+
{ name: 'project/doc.md', direction: 'in', note: 'scope text extracted from doc.md' },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
type: 'generate',
|
|
285
|
+
label: 'Detect semantic duplicates among new epics and against existing on-disk epics — merge overlapping items automatically',
|
|
286
|
+
model: ceremony.stages?.decomposition?.model ?? fallbackModel,
|
|
287
|
+
stageKey: 'decomposition',
|
|
288
|
+
sharedWith: 'Decomposer',
|
|
289
|
+
agent: 'duplicate-detector',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: 'process',
|
|
293
|
+
label: 'Review each Epic\'s stories and split any that mix too many concerns (one LLM call per Epic in parallel)',
|
|
294
|
+
model: ceremony.stages?.decomposition?.model ?? fallbackModel,
|
|
295
|
+
stageKey: 'decomposition',
|
|
296
|
+
sharedWith: 'Decomposer',
|
|
297
|
+
agent: 'story-scope-reviewer',
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: 'context-generation',
|
|
303
|
+
label: 'Context Generation',
|
|
304
|
+
color: 'violet',
|
|
305
|
+
steps: [
|
|
306
|
+
{
|
|
307
|
+
type: 'generate',
|
|
308
|
+
label: 'Write + independently review each Epic/Story context.md (Write → Review → Refine, up to 3 rounds per item)',
|
|
309
|
+
model: ceremony.stages?.['context-generation']?.model ?? fallbackModel,
|
|
310
|
+
stageKey: 'context-generation',
|
|
311
|
+
agents: [
|
|
312
|
+
{ slug: 'context-writer-epic', note: 'writes epic context.md' },
|
|
313
|
+
{ slug: 'context-reviewer-epic', note: 'audits epic context against source JSON' },
|
|
314
|
+
{ slug: 'context-writer-story', note: 'writes story context.md' },
|
|
315
|
+
{ slug: 'context-reviewer-story', note: 'audits story context against source JSON' },
|
|
316
|
+
],
|
|
317
|
+
files: [
|
|
318
|
+
{ name: '{epic}/context.md', direction: 'out' },
|
|
319
|
+
{ name: '{story}/context.md', direction: 'out' },
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'validation',
|
|
326
|
+
label: 'Micro-Check Validation (3-Tier)',
|
|
327
|
+
color: 'amber',
|
|
328
|
+
steps: [
|
|
329
|
+
{
|
|
330
|
+
type: 'generate',
|
|
331
|
+
label: 'Extract project context (once) — infers deployment type, tech stack, cloud/mobile presence to filter validators',
|
|
332
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
333
|
+
stageKey: 'validation',
|
|
334
|
+
agent: 'project-context-extractor',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
type: 'generate',
|
|
338
|
+
label: 'AI-select relevant validators per Epic / Story — excludes inapplicable roles (e.g. cloud if no cloud services). Runs once per Epic, once per Story.',
|
|
339
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
340
|
+
stageKey: 'validation',
|
|
341
|
+
sharedWith: 'Validation',
|
|
342
|
+
agent: 'agent-selector',
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
type: 'validate',
|
|
346
|
+
label: 'Tier 1: Run domain checks per perspective — each check is 1-2 YES/NO LLM calls, run in parallel',
|
|
347
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
348
|
+
stageKey: 'validation',
|
|
349
|
+
sharedWith: 'Validation',
|
|
350
|
+
params: [
|
|
351
|
+
{ key: 'concurrency', label: 'Parallel checks', value: ceremony.stages?.validation?.concurrency ?? 5, min: 1, max: 20 },
|
|
352
|
+
{ key: 'batchSize', label: 'Checks per batch', value: ceremony.stages?.validation?.batchSize ?? 8, min: 1, max: 20 },
|
|
353
|
+
],
|
|
354
|
+
checks: DOMAIN_CHECK_PERSPECTIVES.flatMap(p => [
|
|
355
|
+
{ scope: 'epic', perspective: p, label: `${p} (Epic)` },
|
|
356
|
+
{ scope: 'story', perspective: p, label: `${p} (Story)` },
|
|
357
|
+
]),
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: 'cross',
|
|
361
|
+
label: 'Tier 2: Cross-reference consistency checks — verify perspectives agree (e.g., security \u2194 API, database \u2194 architecture). Runs after Tier 1 using evidence from domain checks.',
|
|
362
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
363
|
+
stageKey: 'validation',
|
|
364
|
+
sharedWith: 'Validation',
|
|
365
|
+
checks: [
|
|
366
|
+
{ scope: 'cross-refs', perspective: 'epic', label: 'Epic Cross-Refs' },
|
|
367
|
+
{ scope: 'cross-refs', perspective: 'story', label: 'Story Cross-Refs' },
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: 'refine',
|
|
372
|
+
label: 'Tier 3: Deterministic scoring + auto-fix critical/major failures (atomic per-check fixes with regression revert)',
|
|
373
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
374
|
+
stageKey: 'validation',
|
|
375
|
+
sharedWith: 'Validation',
|
|
376
|
+
params: [
|
|
377
|
+
{ key: 'maxFixAttempts', label: 'Max fix attempts', value: ceremony.stages?.validation?.maxFixAttempts ?? 3, min: 0, max: 10 },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
type: 'process',
|
|
382
|
+
label: 'If story still below threshold with 15+ ACs and scope issues detected: split into 2-3 focused stories, replace in epic, and re-validate each split story',
|
|
383
|
+
model: ceremony.stages?.validation?.model ?? fallbackModel,
|
|
384
|
+
stageKey: 'validation',
|
|
385
|
+
sharedWith: 'Validation',
|
|
386
|
+
agent: 'story-splitter',
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: 'output',
|
|
392
|
+
label: 'Documentation & Output',
|
|
393
|
+
color: 'emerald',
|
|
394
|
+
steps: [
|
|
395
|
+
{ type: 'process', label: 'Renumber hierarchy IDs + write context.md files with final IDs' },
|
|
396
|
+
{
|
|
397
|
+
type: 'generate',
|
|
398
|
+
label: 'Generate narrative doc.md for each Epic from its canonical context.md',
|
|
399
|
+
model: ceremony.stages?.['doc-generation']?.model ?? fallbackModel,
|
|
400
|
+
stageKey: 'doc-generation',
|
|
401
|
+
agent: 'doc-writer-epic',
|
|
402
|
+
files: [
|
|
403
|
+
{ name: 'project/doc.md', direction: 'in', note: 'project context' },
|
|
404
|
+
{ name: '{epic}/context.md', direction: 'in', note: 'canonical spec — single source of truth' },
|
|
405
|
+
{ name: '{epic}/doc.md', direction: 'out', note: 'narrative documentation' },
|
|
406
|
+
],
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
type: 'generate',
|
|
410
|
+
label: 'Generate narrative doc.md for each Story from its canonical context.md',
|
|
411
|
+
model: ceremony.stages?.['doc-generation']?.model ?? fallbackModel,
|
|
412
|
+
stageKey: 'doc-generation',
|
|
413
|
+
agent: 'doc-writer-story',
|
|
414
|
+
files: [
|
|
415
|
+
{ name: '{epic}/context.md', direction: 'in', note: 'parent epic context' },
|
|
416
|
+
{ name: '{story}/context.md', direction: 'in', note: 'canonical spec — single source of truth' },
|
|
417
|
+
{ name: '{story}/doc.md', direction: 'out', note: 'narrative documentation' },
|
|
418
|
+
],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
type: 'generate',
|
|
422
|
+
label: 'Enrich Story docs with missing implementation detail — API contracts, error tables, DB fields, business rules. Runs once per Story.',
|
|
423
|
+
model: ceremony.stages?.enrichment?.model ?? fallbackModel,
|
|
424
|
+
stageKey: 'enrichment',
|
|
425
|
+
agent: 'story-doc-enricher',
|
|
426
|
+
files: [
|
|
427
|
+
{ name: '{story}/doc.md', direction: 'inout', note: 'read then overwritten with enriched content' },
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: 'output',
|
|
432
|
+
label: 'Write Epic & Story work.json files',
|
|
433
|
+
files: [
|
|
434
|
+
{ name: '{epic}/work.json', direction: 'out' },
|
|
435
|
+
{ name: '{story}/work.json', direction: 'out' },
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
}
|
|
442
|
+
function buildSeedPhases(c) {
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
id: 'decompose',
|
|
446
|
+
label: 'Decompose Story',
|
|
447
|
+
color: 'blue',
|
|
448
|
+
steps: [
|
|
449
|
+
{
|
|
450
|
+
type: 'read',
|
|
451
|
+
label: 'Read story, epic, and project context',
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
type: 'generate',
|
|
455
|
+
label: 'Decompose story into tasks and subtasks',
|
|
456
|
+
model: c?.stages?.decomposition?.model,
|
|
457
|
+
stageKey: 'decomposition',
|
|
458
|
+
agent: 'task-subtask-decomposer',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: 'process',
|
|
462
|
+
label: 'Validate task/subtask structure and IDs (deterministic)',
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
id: 'context-gen',
|
|
468
|
+
label: 'Task Documentation',
|
|
469
|
+
color: 'green',
|
|
470
|
+
steps: [
|
|
471
|
+
{
|
|
472
|
+
type: 'generate',
|
|
473
|
+
label: 'Generate context.md for each task and subtask',
|
|
474
|
+
model: c?.stages?.['context-generation']?.model,
|
|
475
|
+
stageKey: 'context-generation',
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
type: 'output',
|
|
479
|
+
label: 'Write work.json, context.md, and doc.md files',
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
},
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function buildRunPhases(c) {
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
id: 'implement',
|
|
490
|
+
label: 'Code Generation',
|
|
491
|
+
color: 'blue',
|
|
492
|
+
steps: [
|
|
493
|
+
{
|
|
494
|
+
type: 'read',
|
|
495
|
+
label: 'Create git worktree and read documentation chain',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
type: 'generate',
|
|
499
|
+
label: 'AI generates code following traceability rules',
|
|
500
|
+
model: c?.stages?.['code-generation']?.model,
|
|
501
|
+
stageKey: 'code-generation',
|
|
502
|
+
agent: 'code-implementer',
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
id: 'validate',
|
|
508
|
+
label: 'Code Validation',
|
|
509
|
+
color: 'amber',
|
|
510
|
+
steps: [
|
|
511
|
+
{
|
|
512
|
+
type: 'validate',
|
|
513
|
+
label: 'Verify code against quality and traceability checks',
|
|
514
|
+
model: c?.stages?.['code-validation']?.model,
|
|
515
|
+
stageKey: 'code-validation',
|
|
516
|
+
agent: 'code-validator',
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
type: 'generate',
|
|
520
|
+
label: 'Fix violations and regenerate (if needed, up to 3 iterations)',
|
|
521
|
+
model: c?.stages?.['code-generation']?.model,
|
|
522
|
+
stageKey: 'code-generation',
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
id: 'test-merge',
|
|
528
|
+
label: 'Test & Merge',
|
|
529
|
+
color: 'green',
|
|
530
|
+
steps: [
|
|
531
|
+
{
|
|
532
|
+
type: 'validate',
|
|
533
|
+
label: 'Run tests in worktree',
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
type: 'output',
|
|
537
|
+
label: 'Commit, merge to main, update function registry',
|
|
538
|
+
},
|
|
539
|
+
],
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const CEREMONY_WORKFLOWS = {
|
|
545
|
+
'sponsor-call': buildSponsorCallPhases,
|
|
546
|
+
'sprint-planning': (c) => buildSprintPlanningPhases(c),
|
|
547
|
+
'seed': (c) => buildSeedPhases(c),
|
|
548
|
+
'run': (c) => buildRunPhases(c),
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
function resolveModelName(modelId, models) {
|
|
554
|
+
if (!modelId) return '—';
|
|
555
|
+
return models.find((m) => m.modelId === modelId)?.displayName || modelId;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── Inline model select ───────────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
function ModelSelectInline({ value, models, onChange }) {
|
|
561
|
+
const providers = [...new Set(models.map((m) => m.provider))];
|
|
562
|
+
return (
|
|
563
|
+
<select
|
|
564
|
+
value={value || ''}
|
|
565
|
+
onChange={(e) => onChange(e.target.value)}
|
|
566
|
+
onClick={(e) => e.stopPropagation()}
|
|
567
|
+
className="text-xs border border-slate-300 rounded px-1.5 py-0.5 bg-white text-slate-800 focus:outline-none focus:ring-1 focus:ring-blue-400 max-w-[200px]"
|
|
568
|
+
>
|
|
569
|
+
{!value && <option value="">— select —</option>}
|
|
570
|
+
{providers.map((p) => (
|
|
571
|
+
<optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
|
|
572
|
+
{models.filter((m) => m.provider === p).map((m) => (
|
|
573
|
+
<option key={m.modelId} value={m.modelId}>{m.displayName || m.modelId}</option>
|
|
574
|
+
))}
|
|
575
|
+
</optgroup>
|
|
576
|
+
))}
|
|
577
|
+
</select>
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── Agent link ────────────────────────────────────────────────────────────────
|
|
582
|
+
|
|
583
|
+
function AgentLink({ slug, onOpen }) {
|
|
584
|
+
const label = AGENT_LABELS[slug] || slug;
|
|
585
|
+
return (
|
|
586
|
+
<button
|
|
587
|
+
type="button"
|
|
588
|
+
onClick={() => onOpen(slug)}
|
|
589
|
+
className="inline-flex items-center gap-1 text-[10px] font-medium text-blue-600 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 border border-blue-200 hover:border-blue-300 px-1.5 py-0.5 rounded transition-colors whitespace-nowrap"
|
|
590
|
+
title={`Edit ${label} agent instructions`}
|
|
591
|
+
>
|
|
592
|
+
{label}
|
|
593
|
+
<Pencil className="w-2.5 h-2.5 flex-shrink-0" />
|
|
594
|
+
</button>
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function AgentLinks({ step, onOpenAgent }) {
|
|
599
|
+
const items = step.agents
|
|
600
|
+
? step.agents.map((a) => (typeof a === 'string' ? { slug: a, note: null } : a))
|
|
601
|
+
: step.agent
|
|
602
|
+
? [{ slug: step.agent, note: null }]
|
|
603
|
+
: [];
|
|
604
|
+
|
|
605
|
+
if (items.length === 0) return null;
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
<div className="mt-1.5">
|
|
609
|
+
<span className="text-[10px] text-slate-400 font-medium">Agent(s)</span>
|
|
610
|
+
<div className="mt-1 flex flex-col gap-1">
|
|
611
|
+
{items.map(({ slug, note }) => (
|
|
612
|
+
<div key={slug} className="flex items-center gap-2 flex-wrap">
|
|
613
|
+
<AgentLink slug={slug} onOpen={onOpenAgent} />
|
|
614
|
+
{note && <span className="text-[10px] text-slate-400 italic">{note}</span>}
|
|
615
|
+
</div>
|
|
616
|
+
))}
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ── Check links ──────────────────────────────────────────────────────────────
|
|
623
|
+
// Renders clickable check-definition badges inside a scrollable container.
|
|
624
|
+
|
|
625
|
+
function CheckLinks({ step, onOpenCheck }) {
|
|
626
|
+
if (!step.checks || step.checks.length === 0) return null;
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<div className="mt-1.5">
|
|
630
|
+
<span className="text-[10px] text-slate-400 font-medium">Check definitions</span>
|
|
631
|
+
<div className="mt-1 max-h-28 overflow-y-auto border border-slate-100 rounded-md px-1.5 py-1 flex flex-wrap gap-1">
|
|
632
|
+
{step.checks.map((c) => (
|
|
633
|
+
<button
|
|
634
|
+
key={`${c.scope}/${c.perspective}`}
|
|
635
|
+
type="button"
|
|
636
|
+
onClick={() => onOpenCheck({ scope: c.scope, perspective: c.perspective })}
|
|
637
|
+
className="inline-flex items-center gap-1 text-[10px] font-medium text-amber-700 hover:text-amber-900 bg-amber-50 hover:bg-amber-100 border border-amber-200 hover:border-amber-300 px-1.5 py-0.5 rounded transition-colors whitespace-nowrap"
|
|
638
|
+
title={`Edit ${c.scope}/${c.perspective}.json`}
|
|
639
|
+
>
|
|
640
|
+
{c.label}
|
|
641
|
+
<Pencil className="w-2.5 h-2.5 flex-shrink-0" />
|
|
642
|
+
</button>
|
|
643
|
+
))}
|
|
644
|
+
</div>
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── File tags ─────────────────────────────────────────────────────────────────
|
|
650
|
+
// Renders small ← filename (input) / → filename (output) badges on step cards.
|
|
651
|
+
// Steps can carry a `files` array: [{ name, direction: 'in'|'out', note? }]
|
|
652
|
+
|
|
653
|
+
function FileTags({ files, className = '' }) {
|
|
654
|
+
if (!files || files.length === 0) return null;
|
|
655
|
+
return (
|
|
656
|
+
<div className={`flex items-center gap-1 flex-wrap ${className}`}>
|
|
657
|
+
{files.map((f, i) => {
|
|
658
|
+
const isIn = f.direction === 'in';
|
|
659
|
+
const isInOut = f.direction === 'inout';
|
|
660
|
+
const cls = isInOut
|
|
661
|
+
? 'bg-purple-50 text-purple-700 border-purple-200'
|
|
662
|
+
: isIn
|
|
663
|
+
? 'bg-sky-50 text-sky-700 border-sky-200'
|
|
664
|
+
: 'bg-emerald-50 text-emerald-700 border-emerald-200';
|
|
665
|
+
const arrow = isInOut ? '↕' : isIn ? '←' : '→';
|
|
666
|
+
return (
|
|
667
|
+
<span
|
|
668
|
+
key={i}
|
|
669
|
+
className={`inline-flex items-center gap-0.5 text-[10px] font-mono font-medium px-1.5 py-0.5 rounded border whitespace-nowrap ${cls}`}
|
|
670
|
+
title={f.note}
|
|
671
|
+
>
|
|
672
|
+
<span className="font-sans mr-0.5">{arrow}</span>
|
|
673
|
+
{f.name}
|
|
674
|
+
</span>
|
|
675
|
+
);
|
|
676
|
+
})}
|
|
677
|
+
</div>
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// ── Step badge ────────────────────────────────────────────────────────────────
|
|
682
|
+
|
|
683
|
+
function StepBadge({ type }) {
|
|
684
|
+
const cfg = STEP_TYPE_CONFIG[type] || { label: type, cls: 'bg-slate-50 text-slate-600 border-slate-200' };
|
|
685
|
+
return (
|
|
686
|
+
<span className={`inline-block text-xs font-medium px-2 py-0.5 rounded border whitespace-nowrap ${cfg.cls}`}>
|
|
687
|
+
{cfg.label}
|
|
688
|
+
</span>
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── Plain step card ───────────────────────────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
function StepCard({ step, models, editable, onStageModelChange, onValidationModelChange, onStageParamChange, onOpenAgent, onOpenCheck }) {
|
|
695
|
+
const showModel = !['input', 'output', 'read', 'process'].includes(step.type);
|
|
696
|
+
const canEdit = editable && !step.sharedWith && (step.stageKey || step.validationKey);
|
|
697
|
+
|
|
698
|
+
return (
|
|
699
|
+
<div className="bg-white border border-slate-200 rounded-lg px-3 py-2.5 shadow-sm">
|
|
700
|
+
|
|
701
|
+
{/* Row 1: badge + file tags (flex-1) + model — all vertically centred */}
|
|
702
|
+
<div className="flex items-center gap-2">
|
|
703
|
+
<div className="flex-shrink-0">
|
|
704
|
+
<StepBadge type={step.type} />
|
|
705
|
+
</div>
|
|
706
|
+
<FileTags files={step.files} className="flex-1 min-w-0" />
|
|
707
|
+
{showModel && (
|
|
708
|
+
step.sharedWith ? (
|
|
709
|
+
<p className="text-[10px] text-slate-400 italic flex-shrink-0">↑ {step.sharedWith}</p>
|
|
710
|
+
) : canEdit ? (
|
|
711
|
+
<ModelSelectInline
|
|
712
|
+
value={step.model}
|
|
713
|
+
models={models}
|
|
714
|
+
onChange={(modelId) => {
|
|
715
|
+
if (step.stageKey) onStageModelChange(step.stageKey, modelId);
|
|
716
|
+
else onValidationModelChange(step.validationKey, modelId);
|
|
717
|
+
}}
|
|
718
|
+
/>
|
|
719
|
+
) : (
|
|
720
|
+
<p className="text-[10px] text-slate-400 flex-shrink-0">
|
|
721
|
+
{step.note ? step.note : resolveModelName(step.model, models)}
|
|
722
|
+
</p>
|
|
723
|
+
)
|
|
724
|
+
)}
|
|
725
|
+
</div>
|
|
726
|
+
|
|
727
|
+
{/* Row 2: description + agents — tiny left indent */}
|
|
728
|
+
<p className="text-xs text-slate-700 leading-snug text-left mt-2 pl-1">{step.label}</p>
|
|
729
|
+
|
|
730
|
+
<div className="pl-1">
|
|
731
|
+
<AgentLinks step={step} onOpenAgent={onOpenAgent} />
|
|
732
|
+
{onOpenCheck && <CheckLinks step={step} onOpenCheck={onOpenCheck} />}
|
|
733
|
+
</div>
|
|
734
|
+
|
|
735
|
+
{/* Inline params (e.g. concurrency, maxFixAttempts) */}
|
|
736
|
+
{step.params && step.params.length > 0 && (
|
|
737
|
+
<div className="mt-2 pl-1 flex items-center gap-3 flex-wrap">
|
|
738
|
+
{step.params.map((p) => (
|
|
739
|
+
<label key={p.key} className="flex items-center gap-1.5 text-[11px] text-slate-600">
|
|
740
|
+
<span className="font-medium">{p.label}:</span>
|
|
741
|
+
{editable && step.stageKey && onStageParamChange ? (
|
|
742
|
+
<input
|
|
743
|
+
type="number"
|
|
744
|
+
min={p.min}
|
|
745
|
+
max={p.max}
|
|
746
|
+
value={p.value}
|
|
747
|
+
onChange={(e) => onStageParamChange(step.stageKey, p.key, Number(e.target.value))}
|
|
748
|
+
onClick={(e) => e.stopPropagation()}
|
|
749
|
+
className="w-12 text-xs border border-slate-300 rounded px-1.5 py-0.5 bg-white text-slate-800 text-center focus:outline-none focus:ring-1 focus:ring-blue-400"
|
|
750
|
+
/>
|
|
751
|
+
) : (
|
|
752
|
+
<span className="font-mono">{p.value}</span>
|
|
753
|
+
)}
|
|
754
|
+
</label>
|
|
755
|
+
))}
|
|
756
|
+
</div>
|
|
757
|
+
)}
|
|
758
|
+
</div>
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── Loop group card ───────────────────────────────────────────────────────────
|
|
763
|
+
|
|
764
|
+
function LoopGroupCard({ group, models, editable, onStageModelChange, onValidationModelChange, onStageParamChange, onLoopParamChange, onOpenAgent, onOpenCheck }) {
|
|
765
|
+
const { loop, steps } = group;
|
|
766
|
+
const isIndigo = steps.some((s) => s.type === 'cross');
|
|
767
|
+
const c = isIndigo ? LOOP_C.indigo : LOOP_C.amber;
|
|
768
|
+
const hasMulti = steps.length > 1;
|
|
769
|
+
|
|
770
|
+
const canEditLoop = editable && group.loopParamType && !group.loopParamReadOnly;
|
|
771
|
+
|
|
772
|
+
return (
|
|
773
|
+
<div className={`rounded-xl border-2 border-dashed ${c.border} overflow-hidden`}>
|
|
774
|
+
|
|
775
|
+
{/* ── Header ── */}
|
|
776
|
+
<div className={`${c.hdr} border-b-2 border-dashed ${c.hdrBorder} px-3 py-2 flex items-center gap-2 flex-wrap`}>
|
|
777
|
+
<span className={`text-xl font-black leading-none ${c.text}`}>↺</span>
|
|
778
|
+
<span className={`text-xs font-bold ${c.text} tracking-wide`}>Iteration Loop</span>
|
|
779
|
+
<div className="ml-auto flex items-center gap-1.5 flex-wrap">
|
|
780
|
+
{loop.max != null && (
|
|
781
|
+
canEditLoop ? (
|
|
782
|
+
<label className={`text-xs font-semibold ${c.chip} px-2 py-0.5 rounded-full flex items-center gap-1`}>
|
|
783
|
+
up to
|
|
784
|
+
<input
|
|
785
|
+
type="number" min="1" max="200" value={loop.max}
|
|
786
|
+
onChange={(e) => onLoopParamChange(group.loopParamType, 'maxIterations', Number(e.target.value))}
|
|
787
|
+
onClick={(e) => e.stopPropagation()}
|
|
788
|
+
className="w-10 bg-transparent border-b border-current text-center focus:outline-none"
|
|
789
|
+
/>
|
|
790
|
+
iter
|
|
791
|
+
</label>
|
|
792
|
+
) : (
|
|
793
|
+
<span className={`text-xs font-semibold ${c.chip} px-2 py-0.5 rounded-full`}>
|
|
794
|
+
{group.loopParamReadOnly ? `↑ ${loop.max} iter` : `up to ${loop.max} iter`}
|
|
795
|
+
</span>
|
|
796
|
+
)
|
|
797
|
+
)}
|
|
798
|
+
{loop.threshold != null && (
|
|
799
|
+
canEditLoop ? (
|
|
800
|
+
<label className={`text-xs font-semibold ${c.chip} px-2 py-0.5 rounded-full flex items-center gap-1`}>
|
|
801
|
+
≥
|
|
802
|
+
<input
|
|
803
|
+
type="number" min="0" max="100" value={loop.threshold}
|
|
804
|
+
onChange={(e) => onLoopParamChange(group.loopParamType, 'acceptanceThreshold', Number(e.target.value))}
|
|
805
|
+
onClick={(e) => e.stopPropagation()}
|
|
806
|
+
className="w-10 bg-transparent border-b border-current text-center focus:outline-none"
|
|
807
|
+
/>
|
|
808
|
+
/100
|
|
809
|
+
</label>
|
|
810
|
+
) : (
|
|
811
|
+
<span className={`text-xs font-semibold ${c.chip} px-2 py-0.5 rounded-full`}>
|
|
812
|
+
{group.loopParamReadOnly ? `↑ ≥${loop.threshold}/100` : `≥${loop.threshold}/100`}
|
|
813
|
+
</span>
|
|
814
|
+
)
|
|
815
|
+
)}
|
|
816
|
+
</div>
|
|
817
|
+
</div>
|
|
818
|
+
|
|
819
|
+
{/* ── Steps + right-side loop arrow ── */}
|
|
820
|
+
<div className={`${c.bg} p-3 flex gap-2 items-stretch`}>
|
|
821
|
+
<div className="flex-1 flex flex-col">
|
|
822
|
+
{steps.map((step, i) => (
|
|
823
|
+
<div key={i}>
|
|
824
|
+
<StepCard
|
|
825
|
+
step={step}
|
|
826
|
+
models={models}
|
|
827
|
+
editable={editable}
|
|
828
|
+
onStageModelChange={onStageModelChange}
|
|
829
|
+
onValidationModelChange={onValidationModelChange}
|
|
830
|
+
onStageParamChange={onStageParamChange}
|
|
831
|
+
onOpenAgent={onOpenAgent}
|
|
832
|
+
onOpenCheck={onOpenCheck}
|
|
833
|
+
/>
|
|
834
|
+
{i < steps.length - 1 && (
|
|
835
|
+
<div className="flex items-center gap-2 my-1.5 ml-3">
|
|
836
|
+
<div className={`w-px h-4 ${c.condLine}`} />
|
|
837
|
+
<span className={`text-xs ${c.subtext} italic`}>score < threshold → retry</span>
|
|
838
|
+
</div>
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
))}
|
|
842
|
+
</div>
|
|
843
|
+
|
|
844
|
+
{/* Right-side loop-back arrow */}
|
|
845
|
+
{hasMulti && (
|
|
846
|
+
<div className={`flex flex-col items-center flex-shrink-0 ${c.arrow}`} style={{ width: 22 }}>
|
|
847
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
848
|
+
<path d="M2 7 L7 1 L12 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
849
|
+
</svg>
|
|
850
|
+
<div className={`w-0.5 flex-1 ${c.line} my-1 rounded-full`} style={{ minHeight: 36 }} />
|
|
851
|
+
<span
|
|
852
|
+
className={`text-[9px] font-bold tracking-widest uppercase ${c.text} opacity-70`}
|
|
853
|
+
style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}
|
|
854
|
+
>
|
|
855
|
+
retry
|
|
856
|
+
</span>
|
|
857
|
+
</div>
|
|
858
|
+
)}
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ── Phase header ──────────────────────────────────────────────────────────────
|
|
865
|
+
|
|
866
|
+
function PhaseHeader({ label, color }) {
|
|
867
|
+
const cfg = PHASE_COLOR_CONFIG[color] || { dot: 'bg-slate-400', label: 'text-slate-600' };
|
|
868
|
+
return (
|
|
869
|
+
<div className="flex items-center gap-2 mt-5 mb-2">
|
|
870
|
+
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${cfg.dot}`} />
|
|
871
|
+
<span className={`text-xs font-semibold uppercase tracking-wide ${cfg.label}`}>{label}</span>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function Connector() {
|
|
877
|
+
return <div className="w-px h-4 bg-slate-200 ml-4" />;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
881
|
+
|
|
882
|
+
export function CeremonyWorkflowModal({
|
|
883
|
+
ceremony,
|
|
884
|
+
allCeremonies,
|
|
885
|
+
apiKeys,
|
|
886
|
+
models = [],
|
|
887
|
+
missionGenValidation = null,
|
|
888
|
+
onClose,
|
|
889
|
+
onSave,
|
|
890
|
+
onCeremoniesUpdated,
|
|
891
|
+
readOnly = false,
|
|
892
|
+
}) {
|
|
893
|
+
const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(ceremony || {})));
|
|
894
|
+
const [missionGenDraft, setMissionGenDraft] = useState(() =>
|
|
895
|
+
JSON.parse(JSON.stringify(missionGenValidation || { maxIterations: 3, acceptanceThreshold: 95 }))
|
|
896
|
+
);
|
|
897
|
+
const [saving, setSaving] = useState(false);
|
|
898
|
+
const [saveError, setSaveError] = useState(null);
|
|
899
|
+
const [openAgentSlug, setOpenAgentSlug] = useState(null);
|
|
900
|
+
const [openCheck, setOpenCheck] = useState(null); // { scope, perspective } | null
|
|
901
|
+
|
|
902
|
+
// Phases rebuild from draft every render — always reflect current edits
|
|
903
|
+
const buildPhases = ceremony?.name ? CEREMONY_WORKFLOWS[ceremony.name] : null;
|
|
904
|
+
const phases = buildPhases ? buildPhases(draft, missionGenDraft) : null;
|
|
905
|
+
|
|
906
|
+
const isEditable = !readOnly && !!onSave;
|
|
907
|
+
|
|
908
|
+
// ── Update helpers ──────────────────────────────────────────────────────────
|
|
909
|
+
|
|
910
|
+
const updateStageModel = (stageKey, modelId) => {
|
|
911
|
+
const found = models.find((m) => m.modelId === modelId);
|
|
912
|
+
setDraft((prev) => ({
|
|
913
|
+
...prev,
|
|
914
|
+
stages: {
|
|
915
|
+
...prev.stages,
|
|
916
|
+
[stageKey]: {
|
|
917
|
+
...prev.stages?.[stageKey],
|
|
918
|
+
model: modelId,
|
|
919
|
+
provider: found?.provider || prev.stages?.[stageKey]?.provider || '',
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
}));
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const updateValidationModel = (area, modelId) => {
|
|
926
|
+
const found = models.find((m) => m.modelId === modelId);
|
|
927
|
+
if (area === 'top') {
|
|
928
|
+
setDraft((prev) => ({
|
|
929
|
+
...prev,
|
|
930
|
+
validation: {
|
|
931
|
+
...prev.validation,
|
|
932
|
+
model: modelId,
|
|
933
|
+
provider: found?.provider || prev.validation?.provider || '',
|
|
934
|
+
},
|
|
935
|
+
}));
|
|
936
|
+
} else {
|
|
937
|
+
setDraft((prev) => ({
|
|
938
|
+
...prev,
|
|
939
|
+
validation: {
|
|
940
|
+
...prev.validation,
|
|
941
|
+
[area]: {
|
|
942
|
+
...prev.validation?.[area],
|
|
943
|
+
model: modelId,
|
|
944
|
+
provider: found?.provider || prev.validation?.[area]?.provider || '',
|
|
945
|
+
},
|
|
946
|
+
},
|
|
947
|
+
}));
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
const updateStageParam = (stageKey, paramKey, value) => {
|
|
952
|
+
setDraft((prev) => ({
|
|
953
|
+
...prev,
|
|
954
|
+
stages: {
|
|
955
|
+
...prev.stages,
|
|
956
|
+
[stageKey]: {
|
|
957
|
+
...prev.stages?.[stageKey],
|
|
958
|
+
[paramKey]: value,
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
}));
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
const updateLoopParam = (loopParamType, field, value) => {
|
|
965
|
+
if (loopParamType === 'missionGen') {
|
|
966
|
+
setMissionGenDraft((prev) => ({ ...prev, [field]: value }));
|
|
967
|
+
} else if (loopParamType === 'docContext') {
|
|
968
|
+
setDraft((prev) => ({ ...prev, validation: { ...prev.validation, [field]: value } }));
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// ── Save / cancel ───────────────────────────────────────────────────────────
|
|
973
|
+
|
|
974
|
+
const handleSave = async () => {
|
|
975
|
+
setSaving(true);
|
|
976
|
+
setSaveError(null);
|
|
977
|
+
try {
|
|
978
|
+
await onSave(draft, missionGenDraft);
|
|
979
|
+
onClose();
|
|
980
|
+
} catch {
|
|
981
|
+
setSaveError('Failed to save. Please try again.');
|
|
982
|
+
} finally {
|
|
983
|
+
setSaving(false);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
const handleCancel = () => {
|
|
988
|
+
setDraft(JSON.parse(JSON.stringify(ceremony || {})));
|
|
989
|
+
setMissionGenDraft(JSON.parse(JSON.stringify(missionGenValidation || { maxIterations: 3, acceptanceThreshold: 95 })));
|
|
990
|
+
onClose();
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
return (
|
|
994
|
+
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
|
995
|
+
{/* Backdrop: in edit mode clicking outside does nothing to prevent accidental data loss */}
|
|
996
|
+
<div
|
|
997
|
+
className="absolute inset-0 bg-black/40"
|
|
998
|
+
onClick={isEditable ? undefined : onClose}
|
|
999
|
+
/>
|
|
1000
|
+
|
|
1001
|
+
<div
|
|
1002
|
+
className="relative bg-slate-50 rounded-2xl shadow-2xl w-full max-w-xl mx-4 flex flex-col"
|
|
1003
|
+
style={{ height: '85vh' }}
|
|
1004
|
+
>
|
|
1005
|
+
{/* Header */}
|
|
1006
|
+
<div className="flex items-center justify-between px-5 pt-4 pb-3 border-b border-slate-200 flex-shrink-0 bg-white rounded-t-2xl">
|
|
1007
|
+
<div>
|
|
1008
|
+
<h2 className="text-sm font-semibold text-slate-900">
|
|
1009
|
+
{isEditable ? 'Configure Models' : 'Ceremony Workflow'}
|
|
1010
|
+
</h2>
|
|
1011
|
+
<p className="text-xs text-slate-500 mt-0.5">
|
|
1012
|
+
{ceremony?.displayName || ceremony?.name || 'Unknown ceremony'}
|
|
1013
|
+
</p>
|
|
1014
|
+
</div>
|
|
1015
|
+
<div className="flex items-center gap-2">
|
|
1016
|
+
{isEditable && allCeremonies && (
|
|
1017
|
+
<ProviderSwitcherButton
|
|
1018
|
+
ceremonyName={ceremony?.name}
|
|
1019
|
+
ceremonies={allCeremonies.map((c) => c.name === ceremony?.name ? draft : c)}
|
|
1020
|
+
apiKeys={apiKeys}
|
|
1021
|
+
onApplied={(updated) => {
|
|
1022
|
+
const updatedCeremony = updated.find((c) => c.name === ceremony?.name);
|
|
1023
|
+
if (updatedCeremony) setDraft(JSON.parse(JSON.stringify(updatedCeremony)));
|
|
1024
|
+
onCeremoniesUpdated?.(updated);
|
|
1025
|
+
}}
|
|
1026
|
+
/>
|
|
1027
|
+
)}
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
onClick={isEditable ? handleCancel : onClose}
|
|
1031
|
+
className="text-slate-400 hover:text-slate-600 transition-colors"
|
|
1032
|
+
>
|
|
1033
|
+
<X className="w-4 h-4" />
|
|
1034
|
+
</button>
|
|
1035
|
+
</div>
|
|
1036
|
+
</div>
|
|
1037
|
+
|
|
1038
|
+
{/* Scrollable body */}
|
|
1039
|
+
<div className="flex-1 overflow-y-auto px-5 pb-5">
|
|
1040
|
+
{phases === null ? (
|
|
1041
|
+
<div className="flex items-center justify-center h-32">
|
|
1042
|
+
<p className="text-sm text-slate-400">Workflow diagram coming soon for this ceremony.</p>
|
|
1043
|
+
</div>
|
|
1044
|
+
) : (
|
|
1045
|
+
phases.map((phase, pi) => (
|
|
1046
|
+
<div key={phase.id}>
|
|
1047
|
+
<PhaseHeader label={phase.label} color={phase.color} />
|
|
1048
|
+
{phase.steps.map((step, si) => (
|
|
1049
|
+
<div key={si}>
|
|
1050
|
+
{step.type === 'loop-group' ? (
|
|
1051
|
+
<LoopGroupCard
|
|
1052
|
+
group={step}
|
|
1053
|
+
models={models}
|
|
1054
|
+
editable={isEditable}
|
|
1055
|
+
onStageModelChange={updateStageModel}
|
|
1056
|
+
onValidationModelChange={updateValidationModel}
|
|
1057
|
+
onStageParamChange={updateStageParam}
|
|
1058
|
+
onLoopParamChange={updateLoopParam}
|
|
1059
|
+
onOpenAgent={setOpenAgentSlug}
|
|
1060
|
+
onOpenCheck={setOpenCheck}
|
|
1061
|
+
/>
|
|
1062
|
+
) : (
|
|
1063
|
+
<StepCard
|
|
1064
|
+
step={step}
|
|
1065
|
+
models={models}
|
|
1066
|
+
editable={isEditable}
|
|
1067
|
+
onStageModelChange={updateStageModel}
|
|
1068
|
+
onValidationModelChange={updateValidationModel}
|
|
1069
|
+
onStageParamChange={updateStageParam}
|
|
1070
|
+
onOpenAgent={setOpenAgentSlug}
|
|
1071
|
+
onOpenCheck={setOpenCheck}
|
|
1072
|
+
/>
|
|
1073
|
+
)}
|
|
1074
|
+
{si < phase.steps.length - 1 && <Connector />}
|
|
1075
|
+
</div>
|
|
1076
|
+
))}
|
|
1077
|
+
{pi < phases.length - 1 && (
|
|
1078
|
+
<div className="mt-3 border-t border-dashed border-slate-200" />
|
|
1079
|
+
)}
|
|
1080
|
+
</div>
|
|
1081
|
+
))
|
|
1082
|
+
)}
|
|
1083
|
+
</div>
|
|
1084
|
+
|
|
1085
|
+
{/* Footer — edit mode only */}
|
|
1086
|
+
{isEditable && (
|
|
1087
|
+
<div className="flex-shrink-0 border-t border-slate-200 bg-white rounded-b-2xl px-5 py-3 flex items-center justify-between">
|
|
1088
|
+
<div>
|
|
1089
|
+
{saveError && <p className="text-xs text-red-600">{saveError}</p>}
|
|
1090
|
+
</div>
|
|
1091
|
+
<div className="flex items-center gap-2">
|
|
1092
|
+
<button
|
|
1093
|
+
type="button"
|
|
1094
|
+
onClick={handleCancel}
|
|
1095
|
+
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 transition-colors"
|
|
1096
|
+
>
|
|
1097
|
+
Cancel
|
|
1098
|
+
</button>
|
|
1099
|
+
<button
|
|
1100
|
+
type="button"
|
|
1101
|
+
onClick={handleSave}
|
|
1102
|
+
disabled={saving}
|
|
1103
|
+
className="px-4 py-1.5 text-sm font-medium bg-slate-900 text-white rounded-lg hover:bg-slate-700 transition-colors disabled:opacity-40 flex items-center gap-2"
|
|
1104
|
+
>
|
|
1105
|
+
{saving ? (
|
|
1106
|
+
<>
|
|
1107
|
+
<span className="w-3.5 h-3.5 border border-white/40 border-t-white rounded-full animate-spin" />
|
|
1108
|
+
Saving…
|
|
1109
|
+
</>
|
|
1110
|
+
) : 'Save Changes'}
|
|
1111
|
+
</button>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
)}
|
|
1115
|
+
</div>
|
|
1116
|
+
|
|
1117
|
+
{openAgentSlug && (
|
|
1118
|
+
<AgentEditorPopup
|
|
1119
|
+
agentName={`${openAgentSlug}.md`}
|
|
1120
|
+
onClose={() => setOpenAgentSlug(null)}
|
|
1121
|
+
/>
|
|
1122
|
+
)}
|
|
1123
|
+
|
|
1124
|
+
{openCheck && (
|
|
1125
|
+
<CheckEditorPopup
|
|
1126
|
+
scope={openCheck.scope}
|
|
1127
|
+
perspective={openCheck.perspective}
|
|
1128
|
+
onClose={() => setOpenCheck(null)}
|
|
1129
|
+
/>
|
|
1130
|
+
)}
|
|
1131
|
+
</div>
|
|
1132
|
+
);
|
|
1133
|
+
}
|