@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,303 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { readdirSync, readFileSync, existsSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
const __routeDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const LIB_AGENTS_PATH = path.join(__routeDir, '../../../cli/agents');
|
|
9
|
+
const customAgentsDir = (root) => path.join(root, '.avc', 'customized-agents');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default model catalogue — mirrors the defaults in src/cli/init.js.
|
|
13
|
+
* Used as a fallback when a project's avc.json pre-dates model pricing support.
|
|
14
|
+
*/
|
|
15
|
+
const PRICING_SOURCES = {
|
|
16
|
+
claude: 'https://www.anthropic.com/pricing',
|
|
17
|
+
gemini: 'https://ai.google.dev/pricing',
|
|
18
|
+
openai: 'https://openai.com/api/pricing',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const DEFAULT_MODELS = {
|
|
22
|
+
// Anthropic Claude models (prices per 1M tokens in USD)
|
|
23
|
+
'claude-opus-4-6': { provider: 'claude', displayName: 'Claude Opus 4.6', pricing: { input: 5.00, output: 25.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
|
|
24
|
+
'claude-sonnet-4-6': { provider: 'claude', displayName: 'Claude Sonnet 4.6', pricing: { input: 3.00, output: 15.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
|
|
25
|
+
'claude-haiku-4-5-20251001': { provider: 'claude', displayName: 'Claude Haiku 4.5', pricing: { input: 1.00, output: 5.00, unit: 'million', source: PRICING_SOURCES.claude, lastUpdated: '2026-02-24' } },
|
|
26
|
+
// Google Gemini models (prices per 1M tokens in USD)
|
|
27
|
+
'gemini-3.1-pro-preview': { provider: 'gemini', displayName: 'Gemini 3.1 Pro Preview', pricing: { input: 2.00, output: 12.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-03-05' } },
|
|
28
|
+
'gemini-3-flash-preview': { provider: 'gemini', displayName: 'Gemini 3 Flash Preview', pricing: { input: 0.50, output: 3.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-03-05' } },
|
|
29
|
+
'gemini-2.5-pro': { provider: 'gemini', displayName: 'Gemini 2.5 Pro', pricing: { input: 1.25, output: 10.00, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
|
|
30
|
+
'gemini-2.5-flash': { provider: 'gemini', displayName: 'Gemini 2.5 Flash', pricing: { input: 0.30, output: 2.50, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
|
|
31
|
+
'gemini-2.5-flash-lite': { provider: 'gemini', displayName: 'Gemini 2.5 Flash-Lite', pricing: { input: 0.10, output: 0.40, unit: 'million', source: PRICING_SOURCES.gemini, lastUpdated: '2026-02-24' } },
|
|
32
|
+
// OpenAI models (prices per 1M tokens in USD)
|
|
33
|
+
'gpt-5.2': { provider: 'openai', displayName: 'GPT-5.2', pricing: { input: 1.75, output: 14.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
34
|
+
'gpt-5.1': { provider: 'openai', displayName: 'GPT-5.1', pricing: { input: 1.25, output: 10.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
35
|
+
'gpt-5-mini': { provider: 'openai', displayName: 'GPT-5 mini', pricing: { input: 0.25, output: 2.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
36
|
+
'o4-mini': { provider: 'openai', displayName: 'o4-mini', pricing: { input: 1.10, output: 4.40, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
37
|
+
'o3': { provider: 'openai', displayName: 'o3', pricing: { input: 2.00, output: 8.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
38
|
+
'o3-mini': { provider: 'openai', displayName: 'o3-mini', pricing: { input: 0.50, output: 2.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
39
|
+
'gpt-5.2-codex': { provider: 'openai', displayName: 'GPT-5.2-Codex', pricing: { input: 1.75, output: 14.00, unit: 'million', source: PRICING_SOURCES.openai, lastUpdated: '2026-02-24' } },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Settings Router
|
|
44
|
+
* Handles GET /api/settings and PUT sub-routes for project configuration.
|
|
45
|
+
* @param {string} projectRoot - Absolute path to project root
|
|
46
|
+
*/
|
|
47
|
+
export function createSettingsRouter(projectRoot) {
|
|
48
|
+
const router = express.Router();
|
|
49
|
+
const avcJsonPath = path.join(projectRoot, '.avc', 'avc.json');
|
|
50
|
+
const envPath = path.join(projectRoot, '.env');
|
|
51
|
+
|
|
52
|
+
const readAvcConfig = async () => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(await fs.readFile(avcJsonPath, 'utf8'));
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const writeAvcConfig = async (config) => {
|
|
61
|
+
await fs.writeFile(avcJsonPath, JSON.stringify(config, null, 2), 'utf8');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Parse .env into key→value map
|
|
65
|
+
const readEnv = async () => {
|
|
66
|
+
try {
|
|
67
|
+
const lines = (await fs.readFile(envPath, 'utf8')).split('\n');
|
|
68
|
+
const map = {};
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
const m = line.match(/^([A-Z_]+)\s*=\s*(.*)$/);
|
|
71
|
+
if (m) map[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
72
|
+
}
|
|
73
|
+
return map;
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Update or insert a single key in .env, preserving all other lines
|
|
80
|
+
const upsertEnvKey = async (key, value) => {
|
|
81
|
+
let content = '';
|
|
82
|
+
try { content = await fs.readFile(envPath, 'utf8'); } catch {}
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
const idx = lines.findIndex(l => l.match(new RegExp(`^${key}\\s*=`)));
|
|
85
|
+
const newLine = value ? `${key}=${value}` : '';
|
|
86
|
+
if (idx >= 0) {
|
|
87
|
+
if (newLine) {
|
|
88
|
+
lines[idx] = newLine;
|
|
89
|
+
} else {
|
|
90
|
+
lines.splice(idx, 1);
|
|
91
|
+
}
|
|
92
|
+
} else if (newLine) {
|
|
93
|
+
lines.push(newLine);
|
|
94
|
+
}
|
|
95
|
+
await fs.writeFile(envPath, lines.join('\n'), 'utf8');
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// GET /api/settings — snapshot of all configurable settings
|
|
99
|
+
router.get('/', async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const [config, env] = await Promise.all([readAvcConfig(), readEnv()]);
|
|
102
|
+
res.json({
|
|
103
|
+
apiKeys: {
|
|
104
|
+
anthropic: {
|
|
105
|
+
isSet: !!env.ANTHROPIC_API_KEY,
|
|
106
|
+
preview: env.ANTHROPIC_API_KEY ? env.ANTHROPIC_API_KEY.slice(0, 10) + '…' : '',
|
|
107
|
+
},
|
|
108
|
+
gemini: {
|
|
109
|
+
isSet: !!env.GEMINI_API_KEY,
|
|
110
|
+
preview: env.GEMINI_API_KEY ? env.GEMINI_API_KEY.slice(0, 10) + '…' : '',
|
|
111
|
+
},
|
|
112
|
+
openai: {
|
|
113
|
+
isSet: !!env.OPENAI_API_KEY,
|
|
114
|
+
preview: env.OPENAI_API_KEY ? env.OPENAI_API_KEY.slice(0, 10) + '…' : '',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
ceremonies: config?.settings?.ceremonies || [],
|
|
118
|
+
models: (config?.settings?.models && Object.keys(config.settings.models).length > 0)
|
|
119
|
+
? config.settings.models
|
|
120
|
+
: DEFAULT_MODELS,
|
|
121
|
+
missionGenerator: config?.settings?.missionGenerator || { validation: { maxIterations: 3, acceptanceThreshold: 95 } },
|
|
122
|
+
kanbanPort: config?.settings?.kanban?.port || 4174,
|
|
123
|
+
docsPort: config?.settings?.documentation?.port || 4173,
|
|
124
|
+
boardTitle: config?.settings?.kanban?.title || 'AVC Kanban Board',
|
|
125
|
+
costThresholds: config?.settings?.costThresholds || { 'sponsor-call': null, 'sprint-planning': null, 'seed': null },
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
res.status(500).json({ error: err.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// PUT /api/settings/api-keys — only sends keys that are being updated
|
|
133
|
+
router.put('/api-keys', async (req, res) => {
|
|
134
|
+
try {
|
|
135
|
+
const { anthropic, gemini, openai } = req.body;
|
|
136
|
+
if (anthropic !== undefined) await upsertEnvKey('ANTHROPIC_API_KEY', anthropic);
|
|
137
|
+
if (gemini !== undefined) await upsertEnvKey('GEMINI_API_KEY', gemini);
|
|
138
|
+
if (openai !== undefined) await upsertEnvKey('OPENAI_API_KEY', openai);
|
|
139
|
+
res.json({ status: 'ok' });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
res.status(500).json({ error: err.message });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// PUT /api/settings/ceremonies — also accepts missionGenerator alongside ceremonies
|
|
146
|
+
router.put('/ceremonies', async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
const { ceremonies, missionGenerator } = req.body;
|
|
149
|
+
if (!Array.isArray(ceremonies)) {
|
|
150
|
+
return res.status(400).json({ error: 'ceremonies must be an array' });
|
|
151
|
+
}
|
|
152
|
+
const config = await readAvcConfig();
|
|
153
|
+
if (!config.settings) config.settings = {};
|
|
154
|
+
config.settings.ceremonies = ceremonies;
|
|
155
|
+
// Persist missionGenerator validation params if provided
|
|
156
|
+
if (missionGenerator?.validation && typeof missionGenerator.validation === 'object') {
|
|
157
|
+
if (!config.settings.missionGenerator) config.settings.missionGenerator = {};
|
|
158
|
+
config.settings.missionGenerator.validation = {
|
|
159
|
+
maxIterations: Number(missionGenerator.validation.maxIterations) || 3,
|
|
160
|
+
acceptanceThreshold: Number(missionGenerator.validation.acceptanceThreshold) || 95,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
await writeAvcConfig(config);
|
|
164
|
+
res.json({ status: 'ok' });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
res.status(500).json({ error: err.message });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// PUT /api/settings/models — update pricing for all models
|
|
171
|
+
router.put('/models', async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const { models } = req.body;
|
|
174
|
+
if (!models || typeof models !== 'object' || Array.isArray(models)) {
|
|
175
|
+
return res.status(400).json({ error: 'models must be an object' });
|
|
176
|
+
}
|
|
177
|
+
const config = await readAvcConfig();
|
|
178
|
+
if (!config.settings) config.settings = {};
|
|
179
|
+
// Seed from defaults if models have never been persisted (migration for old projects)
|
|
180
|
+
if (!config.settings.models || Object.keys(config.settings.models).length === 0) {
|
|
181
|
+
config.settings.models = JSON.parse(JSON.stringify(DEFAULT_MODELS));
|
|
182
|
+
}
|
|
183
|
+
for (const [modelId, data] of Object.entries(models)) {
|
|
184
|
+
if (!config.settings.models[modelId]) continue; // only update existing models
|
|
185
|
+
if (data.pricing && typeof data.pricing === 'object') {
|
|
186
|
+
const today = new Date().toISOString().split('T')[0];
|
|
187
|
+
config.settings.models[modelId].pricing = {
|
|
188
|
+
input: Number(data.pricing.input) || 0,
|
|
189
|
+
output: Number(data.pricing.output) || 0,
|
|
190
|
+
unit: data.pricing.unit === 'thousand' ? 'thousand' : 'million',
|
|
191
|
+
source: typeof data.pricing.source === 'string' ? data.pricing.source.trim() : '',
|
|
192
|
+
lastUpdated: today,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
await writeAvcConfig(config);
|
|
197
|
+
res.json({ status: 'ok' });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
res.status(500).json({ error: err.message });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// PUT /api/settings/general — board title and/or ports
|
|
204
|
+
router.put('/general', async (req, res) => {
|
|
205
|
+
try {
|
|
206
|
+
const { boardTitle, kanbanPort, docsPort } = req.body;
|
|
207
|
+
const config = await readAvcConfig();
|
|
208
|
+
if (!config.settings) config.settings = {};
|
|
209
|
+
if (!config.settings.kanban) config.settings.kanban = {};
|
|
210
|
+
if (!config.settings.documentation) config.settings.documentation = {};
|
|
211
|
+
if (boardTitle !== undefined) config.settings.kanban.title = boardTitle.trim();
|
|
212
|
+
if (kanbanPort !== undefined) config.settings.kanban.port = Number(kanbanPort);
|
|
213
|
+
if (docsPort !== undefined) config.settings.documentation.port = Number(docsPort);
|
|
214
|
+
await writeAvcConfig(config);
|
|
215
|
+
res.json({ status: 'ok' });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
res.status(500).json({ error: err.message });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// PUT /api/settings/cost-thresholds — update per-ceremony cost limits
|
|
222
|
+
router.put('/cost-thresholds', async (req, res) => {
|
|
223
|
+
try {
|
|
224
|
+
const { thresholds } = req.body;
|
|
225
|
+
const config = await readAvcConfig();
|
|
226
|
+
if (!config.settings) config.settings = {};
|
|
227
|
+
config.settings.costThresholds = thresholds;
|
|
228
|
+
await writeAvcConfig(config);
|
|
229
|
+
res.json({ ok: true });
|
|
230
|
+
} catch (err) {
|
|
231
|
+
res.status(500).json({ error: err.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// GET /api/settings/agents — list all agent names with customization status
|
|
236
|
+
router.get('/agents', (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const names = readdirSync(LIB_AGENTS_PATH).filter(f => f.endsWith('.md')).sort();
|
|
239
|
+
const customDir = customAgentsDir(projectRoot);
|
|
240
|
+
const agents = names.map(name => ({
|
|
241
|
+
name,
|
|
242
|
+
isCustomized: existsSync(path.join(customDir, name)),
|
|
243
|
+
}));
|
|
244
|
+
res.json({ agents });
|
|
245
|
+
} catch (err) {
|
|
246
|
+
res.status(500).json({ error: err.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// GET /api/settings/agents/:name — get agent content (customized or default)
|
|
251
|
+
router.get('/agents/:name', (req, res) => {
|
|
252
|
+
const { name } = req.params;
|
|
253
|
+
if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
|
|
254
|
+
return res.status(400).json({ error: 'Invalid name' });
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const customPath = path.join(customAgentsDir(projectRoot), name);
|
|
258
|
+
const libPath = path.join(LIB_AGENTS_PATH, name);
|
|
259
|
+
if (!existsSync(libPath)) return res.status(404).json({ error: 'Agent not found' });
|
|
260
|
+
const isCustomized = existsSync(customPath);
|
|
261
|
+
const content = readFileSync(isCustomized ? customPath : libPath, 'utf8');
|
|
262
|
+
const defaultContent = readFileSync(libPath, 'utf8');
|
|
263
|
+
res.json({ name, content, isCustomized, defaultContent });
|
|
264
|
+
} catch (err) {
|
|
265
|
+
res.status(500).json({ error: err.message });
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// PUT /api/settings/agents/:name — save customized agent
|
|
270
|
+
router.put('/agents/:name', async (req, res) => {
|
|
271
|
+
const { name } = req.params;
|
|
272
|
+
const { content } = req.body;
|
|
273
|
+
if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
|
|
274
|
+
return res.status(400).json({ error: 'Invalid name' });
|
|
275
|
+
}
|
|
276
|
+
if (typeof content !== 'string') return res.status(400).json({ error: 'content must be a string' });
|
|
277
|
+
try {
|
|
278
|
+
const dir = customAgentsDir(projectRoot);
|
|
279
|
+
await fs.mkdir(dir, { recursive: true });
|
|
280
|
+
await fs.writeFile(path.join(dir, name), content, 'utf8');
|
|
281
|
+
res.json({ status: 'ok' });
|
|
282
|
+
} catch (err) {
|
|
283
|
+
res.status(500).json({ error: err.message });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// DELETE /api/settings/agents/:name — reset to library default
|
|
288
|
+
router.delete('/agents/:name', async (req, res) => {
|
|
289
|
+
const { name } = req.params;
|
|
290
|
+
if (!name.endsWith('.md') || name.includes('/') || name.includes('\\')) {
|
|
291
|
+
return res.status(400).json({ error: 'Invalid name' });
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const customPath = path.join(customAgentsDir(projectRoot), name);
|
|
295
|
+
try { await fs.unlink(customPath); } catch {}
|
|
296
|
+
res.json({ status: 'ok' });
|
|
297
|
+
} catch (err) {
|
|
298
|
+
res.status(500).json({ error: err.message });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return router;
|
|
303
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Setup WebSocket server for real-time updates
|
|
5
|
+
* @param {http.Server} server - HTTP server instance
|
|
6
|
+
* @param {object} dataStore - Data store with work items
|
|
7
|
+
* @returns {WebSocketServer}
|
|
8
|
+
*/
|
|
9
|
+
export function setupWebSocket(server, dataStore, processRegistry = null, ceremonyService = null) {
|
|
10
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
11
|
+
|
|
12
|
+
const clients = new Set();
|
|
13
|
+
|
|
14
|
+
wss.on('connection', (ws) => {
|
|
15
|
+
console.log('WebSocket client connected');
|
|
16
|
+
clients.add(ws);
|
|
17
|
+
|
|
18
|
+
// Send initial data
|
|
19
|
+
ws.send(
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
type: 'init',
|
|
22
|
+
data: {
|
|
23
|
+
message: 'Connected to AVC Kanban Board',
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Send current process list so the client is immediately in sync
|
|
30
|
+
if (processRegistry) {
|
|
31
|
+
ws.send(JSON.stringify({ type: 'process:list', processes: processRegistry.list() }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Send current ceremony status so the client restores running state on reconnect
|
|
35
|
+
if (ceremonyService) {
|
|
36
|
+
const ceremonyStatus = ceremonyService.getStatus();
|
|
37
|
+
if (
|
|
38
|
+
ceremonyStatus.status === 'running' ||
|
|
39
|
+
ceremonyStatus.status === 'cost-limit-pending' ||
|
|
40
|
+
ceremonyStatus.status === 'awaiting-selection'
|
|
41
|
+
) {
|
|
42
|
+
ws.send(JSON.stringify({ type: 'ceremony:sync', ceremonyStatus }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle client messages
|
|
47
|
+
ws.on('message', (message) => {
|
|
48
|
+
try {
|
|
49
|
+
const data = JSON.parse(message);
|
|
50
|
+
console.log('WebSocket message received:', data);
|
|
51
|
+
|
|
52
|
+
// Handle different message types
|
|
53
|
+
if (data.type === 'ping') {
|
|
54
|
+
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error handling WebSocket message:', error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Handle client disconnect
|
|
62
|
+
ws.on('close', () => {
|
|
63
|
+
console.log('WebSocket client disconnected');
|
|
64
|
+
clients.delete(ws);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Handle errors
|
|
68
|
+
ws.on('error', (error) => {
|
|
69
|
+
console.error('WebSocket error:', error);
|
|
70
|
+
clients.delete(ws);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Broadcast message to all connected clients
|
|
76
|
+
* @param {object} message - Message to broadcast
|
|
77
|
+
*/
|
|
78
|
+
function broadcast(message) {
|
|
79
|
+
const payload = JSON.stringify(message);
|
|
80
|
+
|
|
81
|
+
clients.forEach((client) => {
|
|
82
|
+
if (client.readyState === 1) {
|
|
83
|
+
// WebSocket.OPEN
|
|
84
|
+
client.send(payload);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Broadcast work item update
|
|
91
|
+
* @param {string} action - 'added' | 'changed' | 'deleted'
|
|
92
|
+
* @param {string} workItemId - Work item ID
|
|
93
|
+
*/
|
|
94
|
+
function broadcastWorkItemUpdate(action, workItemId) {
|
|
95
|
+
const { items } = dataStore.getHierarchy();
|
|
96
|
+
const item = items.get(workItemId);
|
|
97
|
+
|
|
98
|
+
broadcast({
|
|
99
|
+
type: 'work-item-update',
|
|
100
|
+
action,
|
|
101
|
+
data: {
|
|
102
|
+
id: workItemId,
|
|
103
|
+
item: item ? cleanWorkItemForBroadcast(item) : null,
|
|
104
|
+
timestamp: Date.now(),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Broadcast full refresh (when major changes occur)
|
|
111
|
+
*/
|
|
112
|
+
function broadcastRefresh() {
|
|
113
|
+
broadcast({
|
|
114
|
+
type: 'refresh',
|
|
115
|
+
data: {
|
|
116
|
+
message: 'Work items updated, please refresh',
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function broadcastCeremonyProgress(message) {
|
|
123
|
+
broadcast({ type: 'ceremony:progress', message });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function broadcastCeremonySubstep(substep, meta = {}) {
|
|
127
|
+
broadcast({ type: 'ceremony:substep', substep, meta });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function broadcastCeremonyComplete(result) {
|
|
131
|
+
broadcast({ type: 'ceremony:complete', result });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function broadcastCeremonyError(error) {
|
|
135
|
+
broadcast({ type: 'ceremony:error', error });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function broadcastSprintPlanningProgress(message) {
|
|
139
|
+
broadcast({ type: 'sprint-planning:progress', message });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function broadcastSprintPlanningSubstep(substep, meta) {
|
|
143
|
+
broadcast({ type: 'sprint-planning:substep', substep, meta });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function broadcastSprintPlanningComplete(result) {
|
|
147
|
+
broadcast({ type: 'sprint-planning:complete', result });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function broadcastSprintPlanningError(error) {
|
|
151
|
+
broadcast({ type: 'sprint-planning:error', error });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function broadcastMissionProgress(step, message) {
|
|
155
|
+
broadcast({ type: 'mission:progress', step, message });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function broadcastCostUpdate() {
|
|
159
|
+
broadcast({ type: 'cost:update' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function broadcastCostLimit(cost, threshold, runningType) {
|
|
163
|
+
broadcast({ type: 'ceremony:cost-limit', cost, threshold, runningType });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function broadcastSprintPlanningDecompositionComplete(hierarchy) {
|
|
167
|
+
broadcast({ type: 'sprint-planning:decomposition-complete', hierarchy });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function broadcastSprintPlanningPaused() {
|
|
171
|
+
broadcast({ type: 'sprint-planning:paused' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function broadcastSprintPlanningResumed() {
|
|
175
|
+
broadcast({ type: 'sprint-planning:resumed' });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function broadcastSprintPlanningCancelled() {
|
|
179
|
+
broadcast({ type: 'sprint-planning:cancelled' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function broadcastSprintPlanningDetail(detail) {
|
|
183
|
+
broadcast({ type: 'sprint-planning:detail', detail });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function broadcastCeremonyDetail(detail) {
|
|
187
|
+
broadcast({ type: 'ceremony:detail', detail });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function broadcastCeremonyPaused() {
|
|
191
|
+
broadcast({ type: 'ceremony:paused' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function broadcastCeremonyResumed() {
|
|
195
|
+
broadcast({ type: 'ceremony:resumed' });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function broadcastCeremonyCancelled() {
|
|
199
|
+
broadcast({ type: 'ceremony:cancelled' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function broadcastProcessStarted(record) {
|
|
203
|
+
broadcast({
|
|
204
|
+
type: 'process:started',
|
|
205
|
+
processId: record.id,
|
|
206
|
+
processType: record.type,
|
|
207
|
+
label: record.label,
|
|
208
|
+
startedAt: record.startedAt,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function broadcastProcessStatus(processId, status, extra = {}) {
|
|
213
|
+
broadcast({ type: 'process:status', processId, status, ...extra });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function broadcastRefineProgress(itemId, jobId, message) {
|
|
217
|
+
broadcast({ type: 'refine:progress', itemId, jobId, message });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function broadcastRefineComplete(itemId, jobId, result) {
|
|
221
|
+
broadcast({ type: 'refine:complete', itemId, jobId, result });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function broadcastRefineError(itemId, jobId, error) {
|
|
225
|
+
broadcast({ type: 'refine:error', itemId, jobId, error });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
wss,
|
|
230
|
+
broadcast,
|
|
231
|
+
broadcastWorkItemUpdate,
|
|
232
|
+
broadcastRefresh,
|
|
233
|
+
broadcastCeremonyProgress,
|
|
234
|
+
broadcastCeremonySubstep,
|
|
235
|
+
broadcastCeremonyComplete,
|
|
236
|
+
broadcastCeremonyError,
|
|
237
|
+
broadcastCeremonyPaused,
|
|
238
|
+
broadcastCeremonyResumed,
|
|
239
|
+
broadcastCeremonyCancelled,
|
|
240
|
+
broadcastSprintPlanningProgress,
|
|
241
|
+
broadcastSprintPlanningSubstep,
|
|
242
|
+
broadcastSprintPlanningDetail,
|
|
243
|
+
broadcastSprintPlanningDecompositionComplete,
|
|
244
|
+
broadcastSprintPlanningComplete,
|
|
245
|
+
broadcastSprintPlanningError,
|
|
246
|
+
broadcastSprintPlanningPaused,
|
|
247
|
+
broadcastSprintPlanningResumed,
|
|
248
|
+
broadcastSprintPlanningCancelled,
|
|
249
|
+
broadcastCeremonyDetail,
|
|
250
|
+
broadcastMissionProgress,
|
|
251
|
+
broadcastCostUpdate,
|
|
252
|
+
broadcastCostLimit,
|
|
253
|
+
broadcastProcessStarted,
|
|
254
|
+
broadcastProcessStatus,
|
|
255
|
+
broadcastRefineProgress,
|
|
256
|
+
broadcastRefineComplete,
|
|
257
|
+
broadcastRefineError,
|
|
258
|
+
getClientCount: () => clients.size,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Clean work item for broadcast (remove circular references)
|
|
264
|
+
* @param {object} item - Work item
|
|
265
|
+
* @returns {object} Cleaned work item
|
|
266
|
+
*/
|
|
267
|
+
function cleanWorkItemForBroadcast(item) {
|
|
268
|
+
return {
|
|
269
|
+
id: item.id,
|
|
270
|
+
name: item.name,
|
|
271
|
+
type: item._type,
|
|
272
|
+
status: item.status,
|
|
273
|
+
parentId: item._parentId,
|
|
274
|
+
childrenIds: item._children ? item._children.map((c) => c.id) : [],
|
|
275
|
+
};
|
|
276
|
+
}
|