@agile-vibe-coding/avc 0.1.1 → 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/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 +29 -8
- package/cli/ceremony-history.js +369 -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/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 +0 -0
- package/cli/init-model-config.js +697 -0
- package/cli/init.js +1311 -274
- package/cli/kanban-server-manager.js +228 -0
- package/cli/llm-claude.js +83 -1
- package/cli/llm-gemini.js +85 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +233 -0
- package/cli/llm-provider.js +240 -3
- package/cli/llm-token-limits.js +102 -0
- package/cli/llm-verifier.js +454 -0
- 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 +73 -2
- package/cli/repl-ink.js +4988 -1217
- 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 +2102 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- 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 +18 -5
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fork } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { KanbanLogger } from '../utils/kanban-logger.js';
|
|
6
|
+
import { TokenTracker } from '../../../cli/token-tracker.js';
|
|
7
|
+
import { loadAgent } from '../../../cli/agent-loader.js';
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const PROVIDER_KEY_MAP = {
|
|
11
|
+
claude: 'ANTHROPIC_API_KEY',
|
|
12
|
+
gemini: 'GEMINI_API_KEY',
|
|
13
|
+
openai: 'OPENAI_API_KEY',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* CeremonyService
|
|
18
|
+
* Orchestrates the sponsor-call ceremony from the web UI.
|
|
19
|
+
* Wraps TemplateProcessor and ProjectInitiator methods,
|
|
20
|
+
* manages in-memory ceremony state, and broadcasts WebSocket events.
|
|
21
|
+
*/
|
|
22
|
+
export class CeremonyService {
|
|
23
|
+
constructor(projectRoot) {
|
|
24
|
+
this.projectRoot = projectRoot;
|
|
25
|
+
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
|
|
26
|
+
this.websocket = null;
|
|
27
|
+
this._paused = false;
|
|
28
|
+
this._cancelled = false;
|
|
29
|
+
this._runningType = null; // 'sprint-planning' | 'sponsor-call'
|
|
30
|
+
this._activeProcessId = null; // processId of the currently running ceremony worker
|
|
31
|
+
this._preRunSnapshot = []; // dirs that existed before sprint-planning run
|
|
32
|
+
this._activeChild = null; // forked ChildProcess (if fork-based run)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
pause() {
|
|
36
|
+
this._paused = true;
|
|
37
|
+
if (this._activeChild) {
|
|
38
|
+
// Fork-based: send IPC; worker will reply with { type: 'paused' } which triggers broadcast
|
|
39
|
+
try { this._activeChild.send({ type: 'pause' }); } catch (_) {}
|
|
40
|
+
} else {
|
|
41
|
+
// In-process: broadcast immediately
|
|
42
|
+
if (this._runningType === 'sprint-planning') this.websocket?.broadcastSprintPlanningPaused();
|
|
43
|
+
else this.websocket?.broadcastCeremonyPaused();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resume() {
|
|
48
|
+
this._paused = false;
|
|
49
|
+
if (this._activeChild) {
|
|
50
|
+
try { this._activeChild.send({ type: 'resume' }); } catch (_) {}
|
|
51
|
+
} else {
|
|
52
|
+
if (this._runningType === 'sprint-planning') this.websocket?.broadcastSprintPlanningResumed();
|
|
53
|
+
else this.websocket?.broadcastCeremonyResumed();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
cancel() {
|
|
58
|
+
this._cancelled = true;
|
|
59
|
+
if (this._activeChild) {
|
|
60
|
+
try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
|
|
61
|
+
}
|
|
62
|
+
const isSprintPlanning = this._runningType === 'sprint-planning';
|
|
63
|
+
const msg = 'Waiting for current LLM call to finish…';
|
|
64
|
+
this.state.progress.push({ type: 'detail', detail: msg });
|
|
65
|
+
if (isSprintPlanning) this.websocket?.broadcastSprintPlanningDetail(msg);
|
|
66
|
+
else this.websocket?.broadcastCeremonyDetail(msg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
forceReset() {
|
|
70
|
+
this._cancelled = true;
|
|
71
|
+
this._paused = false;
|
|
72
|
+
if (this._activeChild) {
|
|
73
|
+
try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
|
|
74
|
+
const child = this._activeChild;
|
|
75
|
+
setTimeout(() => { try { child.kill('SIGTERM'); } catch (_) {} }, 3000);
|
|
76
|
+
this._activeChild = null;
|
|
77
|
+
}
|
|
78
|
+
const wasRunningType = this._runningType;
|
|
79
|
+
this._runningType = null;
|
|
80
|
+
this._activeProcessId = null;
|
|
81
|
+
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
|
|
82
|
+
// Broadcast to whichever ceremony was running (or both if unknown)
|
|
83
|
+
if (wasRunningType === 'sprint-planning' || !wasRunningType) {
|
|
84
|
+
this.websocket?.broadcastSprintPlanningCancelled();
|
|
85
|
+
}
|
|
86
|
+
if (wasRunningType === 'sponsor-call' || !wasRunningType) {
|
|
87
|
+
this.websocket?.broadcastCeremonyCancelled();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_cleanupCancelledSprintPlanning() {
|
|
92
|
+
const projectDir = path.join(this.projectRoot, '.avc', 'project');
|
|
93
|
+
if (!fs.existsSync(projectDir)) return;
|
|
94
|
+
const current = fs.readdirSync(projectDir);
|
|
95
|
+
const toDelete = current.filter(d => !this._preRunSnapshot.includes(d));
|
|
96
|
+
for (const d of toDelete) {
|
|
97
|
+
try {
|
|
98
|
+
fs.rmSync(path.join(projectDir, d), { recursive: true, force: true });
|
|
99
|
+
} catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
setWebSocket(ws) {
|
|
104
|
+
this.websocket = ws;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setReloadCallback(fn) {
|
|
108
|
+
this._reloadCallback = fn;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getStatus() {
|
|
112
|
+
return {
|
|
113
|
+
status: this.state.status,
|
|
114
|
+
runningType: this._runningType,
|
|
115
|
+
processId: this._activeProcessId,
|
|
116
|
+
progress: this.state.progress,
|
|
117
|
+
result: this.state.result,
|
|
118
|
+
error: this.state.error,
|
|
119
|
+
costLimitInfo: this.state.costLimitInfo || null,
|
|
120
|
+
decomposedHierarchy: this.state.decomposedHierarchy || null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getAvailableModels() {
|
|
125
|
+
const { default: dotenv } = await import('dotenv');
|
|
126
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
127
|
+
|
|
128
|
+
const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
129
|
+
const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
|
|
130
|
+
const models = avcConfig?.settings?.models || {};
|
|
131
|
+
|
|
132
|
+
return Object.entries(models).map(([modelId, info]) => ({
|
|
133
|
+
modelId,
|
|
134
|
+
displayName: info.displayName,
|
|
135
|
+
provider: info.provider,
|
|
136
|
+
hasApiKey: !!process.env[PROVIDER_KEY_MAP[info.provider]],
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async generateMissionScope(description, modelId, provider, validatorModelId, validatorProvider) {
|
|
141
|
+
const log = new KanbanLogger('mission', this.projectRoot);
|
|
142
|
+
log.info('generateMissionScope() called', {
|
|
143
|
+
description: description.slice(0, 200),
|
|
144
|
+
generatorModel: { provider, modelId },
|
|
145
|
+
validatorModel: { provider: validatorProvider, modelId: validatorModelId },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const { default: dotenv } = await import('dotenv');
|
|
150
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
151
|
+
log.debug('dotenv loaded', { envFile: path.join(this.projectRoot, '.env') });
|
|
152
|
+
|
|
153
|
+
// Read validation settings exclusively from avc.json
|
|
154
|
+
const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
155
|
+
log.debug('Reading avc.json', { path: avcJsonPath });
|
|
156
|
+
const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
|
|
157
|
+
const vs = avcConfig?.settings?.missionGenerator?.validation;
|
|
158
|
+
if (!vs) {
|
|
159
|
+
const err = new Error(
|
|
160
|
+
'Missing settings.missionGenerator.validation in avc.json. ' +
|
|
161
|
+
'Add: { "settings": { "missionGenerator": { "validation": { "maxIterations": 3, "acceptanceThreshold": 75 } } } }'
|
|
162
|
+
);
|
|
163
|
+
log.error('Config missing missionGenerator.validation', { avcJsonPath });
|
|
164
|
+
throw err;
|
|
165
|
+
}
|
|
166
|
+
const maxIterations = vs.maxIterations;
|
|
167
|
+
const acceptanceThreshold = vs.acceptanceThreshold;
|
|
168
|
+
log.info('Validation config loaded', { maxIterations, acceptanceThreshold });
|
|
169
|
+
|
|
170
|
+
// Create LLM providers
|
|
171
|
+
log.debug('Creating LLM providers');
|
|
172
|
+
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
173
|
+
const generatorLLM = await LLMProvider.create(provider, modelId);
|
|
174
|
+
const validatorLLM = await LLMProvider.create(validatorProvider, validatorModelId);
|
|
175
|
+
log.info('LLM providers created', {
|
|
176
|
+
generator: `${provider}/${modelId}`,
|
|
177
|
+
validator: `${validatorProvider}/${validatorModelId}`,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Load agent files
|
|
181
|
+
log.debug('Loading agent files');
|
|
182
|
+
const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
|
|
183
|
+
const validatorAgent = loadAgent('mission-scope-validator.md', this.projectRoot);
|
|
184
|
+
log.debug('Agent files loaded', {
|
|
185
|
+
generatorBytes: generatorAgent.length,
|
|
186
|
+
validatorBytes: validatorAgent.length,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const emit = (step, message) => {
|
|
190
|
+
log.debug(`[WS emit] ${step}: ${message}`);
|
|
191
|
+
this.websocket?.broadcastMissionProgress(step, message);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// ── Step 1: Initial generation ─────────────────────────────────────────
|
|
195
|
+
emit('generating', 'Generating initial mission & scope…');
|
|
196
|
+
const generatorPrompt =
|
|
197
|
+
`The user wants to build:\n\n${description}\n\nGenerate a focused mission statement and initial scope.`;
|
|
198
|
+
log.info('STEP 1: Initial generation — calling generator LLM', {
|
|
199
|
+
model: `${provider}/${modelId}`,
|
|
200
|
+
promptLength: generatorPrompt.length,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let result = await generatorLLM.generateJSON(generatorPrompt, generatorAgent);
|
|
204
|
+
log.info('STEP 1: Generator LLM responded', {
|
|
205
|
+
missionStatement: result.missionStatement,
|
|
206
|
+
initialScope: result.initialScope,
|
|
207
|
+
hasTokenUsage: typeof generatorLLM.getTokenUsage === 'function',
|
|
208
|
+
tokenUsage: typeof generatorLLM.getTokenUsage === 'function' ? generatorLLM.getTokenUsage() : null,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!result.missionStatement || !result.initialScope) {
|
|
212
|
+
log.error('STEP 1: Incomplete output from generator', { result });
|
|
213
|
+
throw new Error('Model returned incomplete output — missing missionStatement or initialScope');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Iterative validate → refine loop ───────────────────────────────────
|
|
217
|
+
let validationResult = null;
|
|
218
|
+
let validationsRun = 0;
|
|
219
|
+
|
|
220
|
+
log.info('Starting validate→refine loop', { maxIterations, acceptanceThreshold });
|
|
221
|
+
|
|
222
|
+
while (validationsRun < maxIterations) {
|
|
223
|
+
// Step 2: Validate
|
|
224
|
+
emit('validating', `Validating result (pass ${validationsRun + 1} of ${maxIterations})…`);
|
|
225
|
+
const validatorPrompt =
|
|
226
|
+
`User description: ${description}\n\n` +
|
|
227
|
+
`Mission Statement: ${result.missionStatement}\n\n` +
|
|
228
|
+
`Initial Scope:\n${result.initialScope}\n\n` +
|
|
229
|
+
`Validate this mission and scope.`;
|
|
230
|
+
|
|
231
|
+
log.info(`STEP 2 [iter ${validationsRun + 1}]: Calling validator LLM`, {
|
|
232
|
+
model: `${validatorProvider}/${validatorModelId}`,
|
|
233
|
+
promptLength: validatorPrompt.length,
|
|
234
|
+
currentMission: result.missionStatement,
|
|
235
|
+
currentScopeLines: result.initialScope.split('\n').length,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
validationResult = await validatorLLM.generateJSON(validatorPrompt, validatorAgent);
|
|
239
|
+
validationsRun++;
|
|
240
|
+
|
|
241
|
+
log.info(`STEP 2 [iter ${validationsRun}]: Validator LLM responded`, {
|
|
242
|
+
overallScore: validationResult.overallScore,
|
|
243
|
+
validationStatus: validationResult.validationStatus,
|
|
244
|
+
readyToUse: validationResult.readyToUse,
|
|
245
|
+
issueCount: validationResult.issues?.length ?? 0,
|
|
246
|
+
issues: validationResult.issues,
|
|
247
|
+
strengths: validationResult.strengths,
|
|
248
|
+
improvementPriorities: validationResult.improvementPriorities,
|
|
249
|
+
tokenUsage: typeof validatorLLM.getTokenUsage === 'function' ? validatorLLM.getTokenUsage() : null,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const score = Number(validationResult.overallScore) || 0;
|
|
253
|
+
log.debug(`Score check: ${score} >= ${acceptanceThreshold}?`, {
|
|
254
|
+
score,
|
|
255
|
+
acceptanceThreshold,
|
|
256
|
+
passes: score >= acceptanceThreshold,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (score >= acceptanceThreshold) {
|
|
260
|
+
emit('done', `Quality check passed — score ${score}/100`);
|
|
261
|
+
log.info(`Loop EXIT: score ${score} >= threshold ${acceptanceThreshold} — accepting result`, {
|
|
262
|
+
validationsRun,
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (validationsRun >= maxIterations) {
|
|
268
|
+
emit('done', `Max iterations reached — score ${score}/100`);
|
|
269
|
+
log.warn(`Loop EXIT: max iterations (${maxIterations}) reached — accepting current result`, {
|
|
270
|
+
finalScore: score,
|
|
271
|
+
validationsRun,
|
|
272
|
+
});
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Step 3: Refine
|
|
277
|
+
emit('refining', `Refining based on ${validationResult.issues?.length ?? 0} issue(s)…`);
|
|
278
|
+
const issues = validationResult.issues || [];
|
|
279
|
+
const issueSummary = issues
|
|
280
|
+
.map(i => `- [${i.severity.toUpperCase()}] ${i.field}: ${i.description} → ${i.suggestion}`)
|
|
281
|
+
.join('\n');
|
|
282
|
+
|
|
283
|
+
const refinePrompt =
|
|
284
|
+
`Original description: ${description}\n\n` +
|
|
285
|
+
`Previous mission statement: ${result.missionStatement}\n` +
|
|
286
|
+
`Previous scope:\n${result.initialScope}\n\n` +
|
|
287
|
+
`Validation score: ${score}/100\n` +
|
|
288
|
+
`Issues to fix:\n${issueSummary}\n\n` +
|
|
289
|
+
`Refine the mission and scope to address these issues.`;
|
|
290
|
+
|
|
291
|
+
log.info(`STEP 3 [iter ${validationsRun}]: Calling generator LLM for refinement`, {
|
|
292
|
+
model: `${provider}/${modelId}`,
|
|
293
|
+
score,
|
|
294
|
+
issueCount: issues.length,
|
|
295
|
+
issueSummary,
|
|
296
|
+
promptLength: refinePrompt.length,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
result = await generatorLLM.generateJSON(refinePrompt, generatorAgent);
|
|
300
|
+
|
|
301
|
+
log.info(`STEP 3 [iter ${validationsRun}]: Generator LLM refinement responded`, {
|
|
302
|
+
missionStatement: result.missionStatement,
|
|
303
|
+
initialScope: result.initialScope,
|
|
304
|
+
tokenUsage: typeof generatorLLM.getTokenUsage === 'function' ? generatorLLM.getTokenUsage() : null,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (!result.missionStatement || !result.initialScope) {
|
|
308
|
+
log.error(`STEP 3 [iter ${validationsRun}]: Refinement returned incomplete output`, { result });
|
|
309
|
+
throw new Error('Refinement returned incomplete output');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const finalScore = validationResult ? Number(validationResult.overallScore) || 0 : null;
|
|
314
|
+
// Only surface issues when the final score did NOT pass the threshold.
|
|
315
|
+
// A passing validation may still return "resolution notes" in issues[] — those are
|
|
316
|
+
// misleading when shown to the user as if they were real problems.
|
|
317
|
+
const finalIssues = (finalScore !== null && finalScore >= acceptanceThreshold)
|
|
318
|
+
? []
|
|
319
|
+
: (validationResult?.issues || []);
|
|
320
|
+
const returnValue = {
|
|
321
|
+
missionStatement: result.missionStatement,
|
|
322
|
+
initialScope: result.initialScope,
|
|
323
|
+
validationScore: finalScore,
|
|
324
|
+
iterations: validationsRun,
|
|
325
|
+
issues: finalIssues,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
log.info('generateMissionScope() completed successfully', {
|
|
329
|
+
validationScore: finalScore,
|
|
330
|
+
iterations: validationsRun,
|
|
331
|
+
issueCount: returnValue.issues.length,
|
|
332
|
+
missionStatement: result.missionStatement,
|
|
333
|
+
});
|
|
334
|
+
log.finish(true, `score=${finalScore} iterations=${validationsRun}`);
|
|
335
|
+
|
|
336
|
+
// Track token usage
|
|
337
|
+
try {
|
|
338
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
339
|
+
const genUsage = generatorLLM.getTokenUsage();
|
|
340
|
+
if (genUsage.totalCalls > 0) {
|
|
341
|
+
tracker.addExecution('mission-scope', {
|
|
342
|
+
input: genUsage.inputTokens,
|
|
343
|
+
output: genUsage.outputTokens,
|
|
344
|
+
provider,
|
|
345
|
+
model: modelId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const valUsage = validatorLLM.getTokenUsage();
|
|
349
|
+
if (valUsage.totalCalls > 0) {
|
|
350
|
+
tracker.addExecution('mission-scope', {
|
|
351
|
+
input: valUsage.inputTokens,
|
|
352
|
+
output: valUsage.outputTokens,
|
|
353
|
+
provider: validatorProvider,
|
|
354
|
+
model: validatorModelId,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
} catch (trackErr) {
|
|
358
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return returnValue;
|
|
362
|
+
|
|
363
|
+
} catch (err) {
|
|
364
|
+
log.error('generateMissionScope() threw an error', {
|
|
365
|
+
message: err.message,
|
|
366
|
+
stack: err.stack,
|
|
367
|
+
});
|
|
368
|
+
log.finish(false, err.message);
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async refineMissionScope(missionStatement, initialScope, refinementRequest, modelId, provider, validatorModelId, validatorProvider) {
|
|
374
|
+
const log = new KanbanLogger('mission-refine', this.projectRoot);
|
|
375
|
+
log.info('refineMissionScope() called', {
|
|
376
|
+
missionStatement: missionStatement.slice(0, 200),
|
|
377
|
+
refinementRequest: refinementRequest.slice(0, 200),
|
|
378
|
+
generatorModel: { provider, modelId },
|
|
379
|
+
validatorModel: { provider: validatorProvider, modelId: validatorModelId },
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const { default: dotenv } = await import('dotenv');
|
|
384
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
385
|
+
|
|
386
|
+
const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
387
|
+
const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
|
|
388
|
+
const vs = avcConfig?.settings?.missionGenerator?.validation;
|
|
389
|
+
if (!vs) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
'Missing settings.missionGenerator.validation in avc.json. ' +
|
|
392
|
+
'Add: { "settings": { "missionGenerator": { "validation": { "maxIterations": 3, "acceptanceThreshold": 75 } } } }'
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const maxIterations = vs.maxIterations;
|
|
396
|
+
const acceptanceThreshold = vs.acceptanceThreshold;
|
|
397
|
+
log.info('Validation config loaded', { maxIterations, acceptanceThreshold });
|
|
398
|
+
|
|
399
|
+
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
400
|
+
const generatorLLM = await LLMProvider.create(provider, modelId);
|
|
401
|
+
const validatorLLM = await LLMProvider.create(validatorProvider, validatorModelId);
|
|
402
|
+
|
|
403
|
+
const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
|
|
404
|
+
const validatorAgent = loadAgent('mission-scope-validator.md', this.projectRoot);
|
|
405
|
+
|
|
406
|
+
const emit = (step, message) => {
|
|
407
|
+
log.debug(`[WS emit] ${step}: ${message}`);
|
|
408
|
+
this.websocket?.broadcastMissionProgress(step, message);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// ── Step 1: Initial refinement ─────────────────────────────────────────
|
|
412
|
+
emit('generating', 'Refining mission & scope…');
|
|
413
|
+
const generatorPrompt =
|
|
414
|
+
`Current mission statement:\n${missionStatement}\n\n` +
|
|
415
|
+
`Current initial scope:\n${initialScope}\n\n` +
|
|
416
|
+
`The user has requested the following refinement:\n${refinementRequest}\n\n` +
|
|
417
|
+
`Refine the mission statement and initial scope accordingly.`;
|
|
418
|
+
log.info('STEP 1: Initial refinement — calling generator LLM', {
|
|
419
|
+
model: `${provider}/${modelId}`,
|
|
420
|
+
promptLength: generatorPrompt.length,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
let result = await generatorLLM.generateJSON(generatorPrompt, generatorAgent);
|
|
424
|
+
log.info('STEP 1: Generator LLM responded', {
|
|
425
|
+
missionStatement: result.missionStatement,
|
|
426
|
+
initialScope: result.initialScope,
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!result.missionStatement || !result.initialScope) {
|
|
430
|
+
log.error('STEP 1: Incomplete output from generator', { result });
|
|
431
|
+
throw new Error('Model returned incomplete output — missing missionStatement or initialScope');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Iterative validate → refine loop ───────────────────────────────────
|
|
435
|
+
let validationResult = null;
|
|
436
|
+
let validationsRun = 0;
|
|
437
|
+
|
|
438
|
+
log.info('Starting validate→refine loop', { maxIterations, acceptanceThreshold });
|
|
439
|
+
|
|
440
|
+
while (validationsRun < maxIterations) {
|
|
441
|
+
emit('validating', `Validating result (pass ${validationsRun + 1} of ${maxIterations})…`);
|
|
442
|
+
const validatorPrompt =
|
|
443
|
+
`Refinement request: ${refinementRequest}\n\n` +
|
|
444
|
+
`Mission Statement: ${result.missionStatement}\n\n` +
|
|
445
|
+
`Initial Scope:\n${result.initialScope}\n\n` +
|
|
446
|
+
`Validate this mission and scope.`;
|
|
447
|
+
|
|
448
|
+
log.info(`STEP 2 [iter ${validationsRun + 1}]: Calling validator LLM`, {
|
|
449
|
+
model: `${validatorProvider}/${validatorModelId}`,
|
|
450
|
+
promptLength: validatorPrompt.length,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
validationResult = await validatorLLM.generateJSON(validatorPrompt, validatorAgent);
|
|
454
|
+
validationsRun++;
|
|
455
|
+
|
|
456
|
+
log.info(`STEP 2 [iter ${validationsRun}]: Validator LLM responded`, {
|
|
457
|
+
overallScore: validationResult.overallScore,
|
|
458
|
+
issueCount: validationResult.issues?.length ?? 0,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const score = Number(validationResult.overallScore) || 0;
|
|
462
|
+
|
|
463
|
+
if (score >= acceptanceThreshold) {
|
|
464
|
+
emit('done', `Quality check passed — score ${score}/100`);
|
|
465
|
+
log.info(`Loop EXIT: score ${score} >= threshold ${acceptanceThreshold}`, { validationsRun });
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (validationsRun >= maxIterations) {
|
|
470
|
+
emit('done', `Max iterations reached — score ${score}/100`);
|
|
471
|
+
log.warn(`Loop EXIT: max iterations (${maxIterations}) reached`, { finalScore: score, validationsRun });
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
emit('refining', `Refining based on ${validationResult.issues?.length ?? 0} issue(s)…`);
|
|
476
|
+
const issues = validationResult.issues || [];
|
|
477
|
+
const issueSummary = issues
|
|
478
|
+
.map(i => `- [${i.severity.toUpperCase()}] ${i.field}: ${i.description} → ${i.suggestion}`)
|
|
479
|
+
.join('\n');
|
|
480
|
+
|
|
481
|
+
const refinePrompt =
|
|
482
|
+
`Refinement request: ${refinementRequest}\n\n` +
|
|
483
|
+
`Previous mission statement: ${result.missionStatement}\n` +
|
|
484
|
+
`Previous scope:\n${result.initialScope}\n\n` +
|
|
485
|
+
`Validation score: ${score}/100\n` +
|
|
486
|
+
`Issues to fix:\n${issueSummary}\n\n` +
|
|
487
|
+
`Refine the mission and scope to address these issues while honouring the refinement request.`;
|
|
488
|
+
|
|
489
|
+
log.info(`STEP 3 [iter ${validationsRun}]: Calling generator LLM for refinement`, {
|
|
490
|
+
model: `${provider}/${modelId}`,
|
|
491
|
+
score,
|
|
492
|
+
issueCount: issues.length,
|
|
493
|
+
promptLength: refinePrompt.length,
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
result = await generatorLLM.generateJSON(refinePrompt, generatorAgent);
|
|
497
|
+
|
|
498
|
+
log.info(`STEP 3 [iter ${validationsRun}]: Generator LLM refinement responded`, {
|
|
499
|
+
missionStatement: result.missionStatement,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
if (!result.missionStatement || !result.initialScope) {
|
|
503
|
+
log.error(`STEP 3 [iter ${validationsRun}]: Refinement returned incomplete output`, { result });
|
|
504
|
+
throw new Error('Refinement returned incomplete output');
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const finalScore = validationResult ? Number(validationResult.overallScore) || 0 : null;
|
|
509
|
+
// Only surface issues when the final score did NOT pass the threshold.
|
|
510
|
+
const finalIssues = (finalScore !== null && finalScore >= acceptanceThreshold)
|
|
511
|
+
? []
|
|
512
|
+
: (validationResult?.issues || []);
|
|
513
|
+
const returnValue = {
|
|
514
|
+
missionStatement: result.missionStatement,
|
|
515
|
+
initialScope: result.initialScope,
|
|
516
|
+
validationScore: finalScore,
|
|
517
|
+
iterations: validationsRun,
|
|
518
|
+
issues: finalIssues,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
log.info('refineMissionScope() completed successfully', {
|
|
522
|
+
validationScore: finalScore,
|
|
523
|
+
iterations: validationsRun,
|
|
524
|
+
issueCount: returnValue.issues.length,
|
|
525
|
+
});
|
|
526
|
+
log.finish(true, `score=${finalScore} iterations=${validationsRun}`);
|
|
527
|
+
|
|
528
|
+
// Track token usage
|
|
529
|
+
try {
|
|
530
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
531
|
+
const genUsage = generatorLLM.getTokenUsage();
|
|
532
|
+
if (genUsage.totalCalls > 0) {
|
|
533
|
+
tracker.addExecution('mission-refine', {
|
|
534
|
+
input: genUsage.inputTokens,
|
|
535
|
+
output: genUsage.outputTokens,
|
|
536
|
+
provider,
|
|
537
|
+
model: modelId,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
const valUsage = validatorLLM.getTokenUsage();
|
|
541
|
+
if (valUsage.totalCalls > 0) {
|
|
542
|
+
tracker.addExecution('mission-refine', {
|
|
543
|
+
input: valUsage.inputTokens,
|
|
544
|
+
output: valUsage.outputTokens,
|
|
545
|
+
provider: validatorProvider,
|
|
546
|
+
model: validatorModelId,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
} catch (trackErr) {
|
|
550
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return returnValue;
|
|
554
|
+
|
|
555
|
+
} catch (err) {
|
|
556
|
+
log.error('refineMissionScope() threw an error', { message: err.message, stack: err.stack });
|
|
557
|
+
log.finish(false, err.message);
|
|
558
|
+
throw err;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async generateCustomArchitecture(description, modelId, provider) {
|
|
563
|
+
const log = new KanbanLogger('arch-custom', this.projectRoot);
|
|
564
|
+
const { default: dotenv } = await import('dotenv');
|
|
565
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
566
|
+
|
|
567
|
+
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
568
|
+
const llm = await LLMProvider.create(provider, modelId);
|
|
569
|
+
const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
|
|
570
|
+
|
|
571
|
+
const prompt =
|
|
572
|
+
`The user wants a SINGLE custom architecture for their project.\n\n` +
|
|
573
|
+
`User description: ${description}\n\n` +
|
|
574
|
+
`Return a JSON object with EXACTLY ONE architecture in the "architectures" array ` +
|
|
575
|
+
`matching the user's description. Include all required fields: ` +
|
|
576
|
+
`name, description, requiresCloudProvider, bestFor, costTier. ` +
|
|
577
|
+
`Optionally include migrationPath if applicable.`;
|
|
578
|
+
|
|
579
|
+
const result = await llm.generateJSON(prompt, agentInstruction);
|
|
580
|
+
const arch = (result.architectures || [])[0];
|
|
581
|
+
if (!arch?.name || !arch?.description) {
|
|
582
|
+
throw new Error('Model returned incomplete architecture — missing name or description');
|
|
583
|
+
}
|
|
584
|
+
log.info('generateCustomArchitecture result', { archName: arch.name });
|
|
585
|
+
return arch;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async refineCustomArchitecture(currentArch, refinementRequest, modelId, provider) {
|
|
589
|
+
const log = new KanbanLogger('arch-custom', this.projectRoot);
|
|
590
|
+
const { default: dotenv } = await import('dotenv');
|
|
591
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
592
|
+
|
|
593
|
+
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
594
|
+
const llm = await LLMProvider.create(provider, modelId);
|
|
595
|
+
const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
|
|
596
|
+
|
|
597
|
+
const prompt =
|
|
598
|
+
`Refine the following architecture based on the user's request.\n\n` +
|
|
599
|
+
`Current architecture: ${JSON.stringify(currentArch, null, 2)}\n\n` +
|
|
600
|
+
`Refinement request: ${refinementRequest}\n\n` +
|
|
601
|
+
`Return a JSON object with EXACTLY ONE updated architecture in the "architectures" array.`;
|
|
602
|
+
|
|
603
|
+
const result = await llm.generateJSON(prompt, agentInstruction);
|
|
604
|
+
const arch = (result.architectures || [])[0];
|
|
605
|
+
if (!arch?.name || !arch?.description) {
|
|
606
|
+
throw new Error('Model returned incomplete architecture');
|
|
607
|
+
}
|
|
608
|
+
log.info('refineCustomArchitecture result', { archName: arch.name });
|
|
609
|
+
return arch;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async analyzeDatabase(mission, scope, strategy) {
|
|
613
|
+
const log = new KanbanLogger('analyze-db', this.projectRoot);
|
|
614
|
+
log.info('analyzeDatabase() called', {
|
|
615
|
+
missionLength: mission?.length,
|
|
616
|
+
scopeLines: scope?.split('\n').length,
|
|
617
|
+
strategy,
|
|
618
|
+
});
|
|
619
|
+
try {
|
|
620
|
+
const { TemplateProcessor } = await import('../../../cli/template-processor.js');
|
|
621
|
+
const p = new TemplateProcessor('sponsor-call', null, true);
|
|
622
|
+
log.debug('Calling getDatabaseRecommendation');
|
|
623
|
+
const result = await p.getDatabaseRecommendation(mission, scope, strategy);
|
|
624
|
+
log.info('analyzeDatabase() completed', { resultKeys: Object.keys(result || {}) });
|
|
625
|
+
|
|
626
|
+
// Track token usage from TemplateProcessor's internal providers
|
|
627
|
+
try {
|
|
628
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
629
|
+
if (p._stageProviders) {
|
|
630
|
+
for (const providerInstance of Object.values(p._stageProviders)) {
|
|
631
|
+
if (typeof providerInstance.getTokenUsage === 'function') {
|
|
632
|
+
const usage = providerInstance.getTokenUsage();
|
|
633
|
+
if (usage.totalCalls > 0) {
|
|
634
|
+
tracker.addExecution('analyze-database', {
|
|
635
|
+
input: usage.inputTokens,
|
|
636
|
+
output: usage.outputTokens,
|
|
637
|
+
provider: usage.provider,
|
|
638
|
+
model: usage.model,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
} catch (trackErr) {
|
|
645
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
log.finish(true);
|
|
649
|
+
return result;
|
|
650
|
+
} catch (err) {
|
|
651
|
+
log.error('analyzeDatabase() failed', { message: err.message, stack: err.stack });
|
|
652
|
+
log.finish(false, err.message);
|
|
653
|
+
throw err;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async analyzeArchitecture(mission, scope, dbContext, strategy) {
|
|
658
|
+
const log = new KanbanLogger('analyze-arch', this.projectRoot);
|
|
659
|
+
log.info('analyzeArchitecture() called', {
|
|
660
|
+
missionLength: mission?.length,
|
|
661
|
+
scopeLines: scope?.split('\n').length,
|
|
662
|
+
dbContext: dbContext ? 'provided' : 'null',
|
|
663
|
+
strategy,
|
|
664
|
+
});
|
|
665
|
+
try {
|
|
666
|
+
const { TemplateProcessor } = await import('../../../cli/template-processor.js');
|
|
667
|
+
const p = new TemplateProcessor('sponsor-call', null, true);
|
|
668
|
+
log.debug('Calling getArchitectureRecommendations');
|
|
669
|
+
const result = await p.getArchitectureRecommendations(mission, scope, dbContext, strategy);
|
|
670
|
+
log.info('analyzeArchitecture() completed', { resultKeys: Object.keys(result || {}) });
|
|
671
|
+
|
|
672
|
+
// Track token usage from TemplateProcessor's internal providers
|
|
673
|
+
try {
|
|
674
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
675
|
+
if (p._stageProviders) {
|
|
676
|
+
for (const providerInstance of Object.values(p._stageProviders)) {
|
|
677
|
+
if (typeof providerInstance.getTokenUsage === 'function') {
|
|
678
|
+
const usage = providerInstance.getTokenUsage();
|
|
679
|
+
if (usage.totalCalls > 0) {
|
|
680
|
+
tracker.addExecution('analyze-architecture', {
|
|
681
|
+
input: usage.inputTokens,
|
|
682
|
+
output: usage.outputTokens,
|
|
683
|
+
provider: usage.provider,
|
|
684
|
+
model: usage.model,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
} catch (trackErr) {
|
|
691
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
log.finish(true);
|
|
695
|
+
return result;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
log.error('analyzeArchitecture() failed', { message: err.message, stack: err.stack });
|
|
698
|
+
log.finish(false, err.message);
|
|
699
|
+
throw err;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async prefillAnswers(mission, scope, arch, dbContext, strategy) {
|
|
704
|
+
const log = new KanbanLogger('prefill', this.projectRoot);
|
|
705
|
+
log.info('prefillAnswers() called', {
|
|
706
|
+
missionLength: mission?.length,
|
|
707
|
+
scopeLines: scope?.split('\n').length,
|
|
708
|
+
arch: arch ? 'provided' : 'null',
|
|
709
|
+
dbContext: dbContext ? 'provided' : 'null',
|
|
710
|
+
strategy,
|
|
711
|
+
});
|
|
712
|
+
try {
|
|
713
|
+
const { TemplateProcessor } = await import('../../../cli/template-processor.js');
|
|
714
|
+
const p = new TemplateProcessor('sponsor-call', null, true);
|
|
715
|
+
log.debug('Calling prefillQuestions');
|
|
716
|
+
// cloudProvider is null — we let the architecture name carry that context
|
|
717
|
+
const result = await p.prefillQuestions(mission, scope, arch, null, dbContext, strategy);
|
|
718
|
+
log.info('prefillAnswers() completed', { resultKeys: Object.keys(result || {}) });
|
|
719
|
+
|
|
720
|
+
// Track token usage from TemplateProcessor's internal providers
|
|
721
|
+
try {
|
|
722
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
723
|
+
if (p._stageProviders) {
|
|
724
|
+
for (const providerInstance of Object.values(p._stageProviders)) {
|
|
725
|
+
if (typeof providerInstance.getTokenUsage === 'function') {
|
|
726
|
+
const usage = providerInstance.getTokenUsage();
|
|
727
|
+
if (usage.totalCalls > 0) {
|
|
728
|
+
tracker.addExecution('prefill-answers', {
|
|
729
|
+
input: usage.inputTokens,
|
|
730
|
+
output: usage.outputTokens,
|
|
731
|
+
provider: usage.provider,
|
|
732
|
+
model: usage.model,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} catch (trackErr) {
|
|
739
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
log.finish(true);
|
|
743
|
+
return result;
|
|
744
|
+
} catch (err) {
|
|
745
|
+
log.error('prefillAnswers() failed', { message: err.message, stack: err.stack });
|
|
746
|
+
log.finish(false, err.message);
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// ── Fork-based ceremony execution ───────────────────────────────────────────
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Shared dispatcher for worker IPC messages.
|
|
755
|
+
* Used both by the direct-fork path (child.on('message')) and the
|
|
756
|
+
* IPC relay path (handleWorkerMessage called from start.js).
|
|
757
|
+
* @param {object} msg - Worker IPC message
|
|
758
|
+
* @param {object} record - ProcessRegistry record
|
|
759
|
+
* @param {ProcessRegistry} registry
|
|
760
|
+
*/
|
|
761
|
+
async _dispatchWorkerMessage(msg, record, registry) {
|
|
762
|
+
const entry = { ts: Date.now(), level: 'info', text: msg.message || msg.substep || msg.detail || '' };
|
|
763
|
+
const isSP = record.type === 'sprint-planning';
|
|
764
|
+
switch (msg.type) {
|
|
765
|
+
case 'progress':
|
|
766
|
+
registry.appendLog(record.id, entry);
|
|
767
|
+
this.state.progress.push({ type: 'progress', message: msg.message });
|
|
768
|
+
if (isSP) this.websocket?.broadcastSprintPlanningProgress(msg.message);
|
|
769
|
+
else this.websocket?.broadcastCeremonyProgress(msg.message);
|
|
770
|
+
break;
|
|
771
|
+
case 'substep':
|
|
772
|
+
entry.level = 'detail'; entry.text = msg.substep;
|
|
773
|
+
registry.appendLog(record.id, entry);
|
|
774
|
+
this.state.progress.push({ type: 'substep', substep: msg.substep, meta: msg.meta });
|
|
775
|
+
if (isSP) this.websocket?.broadcastSprintPlanningSubstep(msg.substep, msg.meta);
|
|
776
|
+
else this.websocket?.broadcastCeremonySubstep(msg.substep, msg.meta);
|
|
777
|
+
break;
|
|
778
|
+
case 'detail':
|
|
779
|
+
entry.level = 'detail'; entry.text = msg.detail;
|
|
780
|
+
registry.appendLog(record.id, entry);
|
|
781
|
+
this.state.progress.push({ type: 'detail', detail: msg.detail });
|
|
782
|
+
if (isSP) this.websocket?.broadcastSprintPlanningDetail(msg.detail);
|
|
783
|
+
else this.websocket?.broadcastCeremonyDetail(msg.detail);
|
|
784
|
+
break;
|
|
785
|
+
case 'paused':
|
|
786
|
+
registry.setStatus(record.id, 'paused');
|
|
787
|
+
if (isSP) this.websocket?.broadcastSprintPlanningPaused();
|
|
788
|
+
else this.websocket?.broadcastCeremonyPaused();
|
|
789
|
+
break;
|
|
790
|
+
case 'resumed':
|
|
791
|
+
registry.setStatus(record.id, 'running');
|
|
792
|
+
if (isSP) this.websocket?.broadcastSprintPlanningResumed();
|
|
793
|
+
else this.websocket?.broadcastCeremonyResumed();
|
|
794
|
+
break;
|
|
795
|
+
case 'complete':
|
|
796
|
+
this.state.status = 'complete';
|
|
797
|
+
this.state.result = msg.result;
|
|
798
|
+
this._activeChild = null;
|
|
799
|
+
this._activeProcessId = null;
|
|
800
|
+
registry.setStatus(record.id, 'complete', { result: msg.result });
|
|
801
|
+
await this._reloadCallback?.();
|
|
802
|
+
if (isSP) this.websocket?.broadcastSprintPlanningComplete(msg.result);
|
|
803
|
+
else this.websocket?.broadcastCeremonyComplete(msg.result);
|
|
804
|
+
this.websocket?.broadcastRefresh();
|
|
805
|
+
break;
|
|
806
|
+
case 'cancelled':
|
|
807
|
+
if (isSP) this._cleanupCancelledSprintPlanning();
|
|
808
|
+
this.state.status = 'idle';
|
|
809
|
+
this._activeChild = null;
|
|
810
|
+
this._activeProcessId = null;
|
|
811
|
+
registry.setStatus(record.id, 'cancelled');
|
|
812
|
+
if (isSP) this.websocket?.broadcastSprintPlanningCancelled();
|
|
813
|
+
else this.websocket?.broadcastCeremonyCancelled();
|
|
814
|
+
break;
|
|
815
|
+
case 'error':
|
|
816
|
+
this.state.status = 'error';
|
|
817
|
+
this.state.error = msg.error;
|
|
818
|
+
this._activeChild = null;
|
|
819
|
+
this._activeProcessId = null;
|
|
820
|
+
registry.setStatus(record.id, 'error', { error: msg.error });
|
|
821
|
+
if (isSP) this.websocket?.broadcastSprintPlanningError(msg.error);
|
|
822
|
+
else this.websocket?.broadcastCeremonyError(msg.error);
|
|
823
|
+
break;
|
|
824
|
+
case 'cost-limit': {
|
|
825
|
+
const pauseMsg = `Cost limit reached: $${msg.cost.toFixed(4)} spent (limit: $${(msg.threshold ?? 0).toFixed(2)}). Ceremony paused — waiting for user decision.`;
|
|
826
|
+
this.state.progress.push({ type: 'progress', message: pauseMsg });
|
|
827
|
+
this.state.status = 'cost-limit-pending';
|
|
828
|
+
this.state.costLimitInfo = { cost: msg.cost, threshold: msg.threshold };
|
|
829
|
+
// _activeChild stays alive — worker is waiting for cost-limit-continue or cancel
|
|
830
|
+
registry.setStatus(record.id, 'paused');
|
|
831
|
+
this.websocket?.broadcastCostLimit(msg.cost, msg.threshold, this._runningType);
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
case 'decomposition-complete':
|
|
835
|
+
this.state.status = 'awaiting-selection';
|
|
836
|
+
this.state.decomposedHierarchy = msg.hierarchy;
|
|
837
|
+
// _activeChild stays alive — worker is polling for selection-confirmed or cancel
|
|
838
|
+
registry.setStatus(record.id, 'paused');
|
|
839
|
+
this.websocket?.broadcastSprintPlanningDecompositionComplete(msg.hierarchy);
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Resume sprint planning with the user's epic/story selection.
|
|
846
|
+
* Sends selection-confirmed to the waiting worker and restores running state.
|
|
847
|
+
*/
|
|
848
|
+
confirmSprintPlanningSelection(selectedEpicIds, selectedStoryIds) {
|
|
849
|
+
if (this._activeChild) {
|
|
850
|
+
try {
|
|
851
|
+
this._activeChild.send({ type: 'selection-confirmed', selectedEpicIds, selectedStoryIds });
|
|
852
|
+
} catch (_) {}
|
|
853
|
+
}
|
|
854
|
+
this.state.status = 'running';
|
|
855
|
+
this.state.decomposedHierarchy = null;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Resume ceremony past the cost limit (user chose "Continue Anyway").
|
|
860
|
+
* Sends cost-limit-continue to the waiting worker and restores running state.
|
|
861
|
+
*/
|
|
862
|
+
continuePastCostLimit() {
|
|
863
|
+
if (this._activeChild) {
|
|
864
|
+
try { this._activeChild.send({ type: 'cost-limit-continue' }); } catch (_) {}
|
|
865
|
+
}
|
|
866
|
+
this.state.status = 'running';
|
|
867
|
+
this.state.costLimitInfo = null;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Read the cost threshold for a ceremony type from avc.json.
|
|
872
|
+
* Returns null if not configured (unlimited).
|
|
873
|
+
*/
|
|
874
|
+
_getCostThreshold(ceremonyType) {
|
|
875
|
+
try {
|
|
876
|
+
const config = JSON.parse(fs.readFileSync(path.join(this.projectRoot, '.avc', 'avc.json'), 'utf8'));
|
|
877
|
+
return config.settings?.costThresholds?.[ceremonyType] ?? null;
|
|
878
|
+
} catch { return null; }
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ── Public relay entry-points (called from start.js when running as CLI fork) ─
|
|
882
|
+
|
|
883
|
+
/** Relay a worker IPC message received via CLI → Kanban IPC channel. */
|
|
884
|
+
handleWorkerMessage(processId, msg) {
|
|
885
|
+
const record = this._registry?.getByProcessId(processId);
|
|
886
|
+
if (record) this._dispatchWorkerMessage(msg, record, this._registry);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Relay worker exit notification received via CLI → Kanban IPC channel. */
|
|
890
|
+
handleWorkerExit(processId, code) {
|
|
891
|
+
const record = this._registry?.getByProcessId(processId);
|
|
892
|
+
if (!record) return;
|
|
893
|
+
this._activeChild = null;
|
|
894
|
+
this._activeProcessId = null;
|
|
895
|
+
const isSP = record.type === 'sprint-planning';
|
|
896
|
+
const wasActive = this._runningType === record.type &&
|
|
897
|
+
(this.state.status === 'running' || this.state.status === 'cost-limit-pending');
|
|
898
|
+
if (wasActive) {
|
|
899
|
+
const error = `Worker exited unexpectedly (code ${code})`;
|
|
900
|
+
this._registry.setStatus(record.id, 'error', { error });
|
|
901
|
+
this.state.status = 'error';
|
|
902
|
+
this.state.error = error;
|
|
903
|
+
this.state.costLimitInfo = null;
|
|
904
|
+
if (isSP) this.websocket?.broadcastSprintPlanningError(error);
|
|
905
|
+
else this.websocket?.broadcastCeremonyError(error);
|
|
906
|
+
}
|
|
907
|
+
this._runningType = null;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Called when CLI confirms it has forked the worker (informational). */
|
|
911
|
+
handleWorkerStarted(processId, pid) {
|
|
912
|
+
// Worker forked by CLI — no additional action required in Kanban
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Run sprint planning in a forked child process.
|
|
917
|
+
* When running as a fork of the CLI (process.connected), uses IPC relay mode:
|
|
918
|
+
* the CLI forks the worker and relays messages via its IPC channel.
|
|
919
|
+
* @param {ProcessRegistry} registry
|
|
920
|
+
* @returns {string} processId
|
|
921
|
+
*/
|
|
922
|
+
async runSprintPlanningInProcess(registry) {
|
|
923
|
+
if (this.state.status === 'running') {
|
|
924
|
+
throw new Error('Ceremony already running');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const projectDir = path.join(this.projectRoot, '.avc', 'project');
|
|
928
|
+
this._preRunSnapshot = fs.existsSync(projectDir) ? fs.readdirSync(projectDir) : [];
|
|
929
|
+
this._paused = false;
|
|
930
|
+
this._cancelled = false;
|
|
931
|
+
this._runningType = 'sprint-planning';
|
|
932
|
+
this.state = { status: 'running', progress: [], result: null, error: null };
|
|
933
|
+
|
|
934
|
+
const record = registry.create('sprint-planning', 'Sprint Planning');
|
|
935
|
+
this._registry = registry;
|
|
936
|
+
this._activeProcessId = record.id;
|
|
937
|
+
|
|
938
|
+
const costThreshold = this._getCostThreshold('sprint-planning');
|
|
939
|
+
|
|
940
|
+
if (process.connected) {
|
|
941
|
+
// IPC relay mode — proxy stands in for the worker child so that pause/resume/cancel
|
|
942
|
+
// continue to work unchanged; actual forking is delegated to the CLI process.
|
|
943
|
+
const proxy = {
|
|
944
|
+
send: (m) => { try { process.send({ type: 'ceremony:control', action: m.type, processId: record.id, payload: m }); } catch (_) {} },
|
|
945
|
+
kill: (s) => { try { process.send({ type: 'ceremony:kill', signal: s, processId: record.id }); } catch (_) {} },
|
|
946
|
+
};
|
|
947
|
+
this._activeChild = proxy;
|
|
948
|
+
registry.attach(record.id, proxy);
|
|
949
|
+
process.send({ type: 'ceremony:fork', ceremonyType: 'sprint-planning', processId: record.id, costThreshold });
|
|
950
|
+
return record.id;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Standalone fallback — direct fork (used for tests / manual server launch)
|
|
954
|
+
const workerPath = path.join(__dirname, '../workers/sprint-planning-worker.js');
|
|
955
|
+
const child = fork(workerPath, [], {
|
|
956
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
957
|
+
env: { ...process.env },
|
|
958
|
+
});
|
|
959
|
+
child.stdout?.on('data', d => process.stdout.write(d));
|
|
960
|
+
child.stderr?.on('data', d => process.stderr.write(d));
|
|
961
|
+
|
|
962
|
+
registry.attach(record.id, child);
|
|
963
|
+
this._activeChild = child;
|
|
964
|
+
child.send({ type: 'init', costThreshold });
|
|
965
|
+
|
|
966
|
+
child.on('message', (msg) => this._dispatchWorkerMessage(msg, record, registry));
|
|
967
|
+
|
|
968
|
+
child.on('exit', (code) => {
|
|
969
|
+
this._activeChild = null;
|
|
970
|
+
this._activeProcessId = null;
|
|
971
|
+
if (this._runningType === 'sprint-planning' && this.state.status === 'running') {
|
|
972
|
+
registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
|
|
973
|
+
this.state.status = 'error';
|
|
974
|
+
this.state.error = `Worker exited unexpectedly (code ${code})`;
|
|
975
|
+
this.websocket?.broadcastSprintPlanningError(this.state.error);
|
|
976
|
+
}
|
|
977
|
+
this._runningType = null;
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return record.id;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Run sponsor call in a forked child process.
|
|
985
|
+
* When running as a fork of the CLI (process.connected), uses IPC relay mode.
|
|
986
|
+
* @param {ProcessRegistry} registry
|
|
987
|
+
* @param {object} requirements - All 7 template variables
|
|
988
|
+
* @returns {string} processId
|
|
989
|
+
*/
|
|
990
|
+
async runSponsorCallInProcess(registry, requirements) {
|
|
991
|
+
if (this.state.status === 'running') {
|
|
992
|
+
throw new Error('Ceremony already running');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
this._paused = false;
|
|
996
|
+
this._cancelled = false;
|
|
997
|
+
this._runningType = 'sponsor-call';
|
|
998
|
+
this.state = { status: 'running', progress: [], result: null, error: null };
|
|
999
|
+
|
|
1000
|
+
const record = registry.create('sponsor-call', 'Sponsor Call');
|
|
1001
|
+
this._registry = registry;
|
|
1002
|
+
this._activeProcessId = record.id;
|
|
1003
|
+
|
|
1004
|
+
const costThreshold = this._getCostThreshold('sponsor-call');
|
|
1005
|
+
|
|
1006
|
+
if (process.connected) {
|
|
1007
|
+
// IPC relay mode
|
|
1008
|
+
const proxy = {
|
|
1009
|
+
send: (m) => { try { process.send({ type: 'ceremony:control', action: m.type, processId: record.id, payload: m }); } catch (_) {} },
|
|
1010
|
+
kill: (s) => { try { process.send({ type: 'ceremony:kill', signal: s, processId: record.id }); } catch (_) {} },
|
|
1011
|
+
};
|
|
1012
|
+
this._activeChild = proxy;
|
|
1013
|
+
registry.attach(record.id, proxy);
|
|
1014
|
+
process.send({ type: 'ceremony:fork', ceremonyType: 'sponsor-call', processId: record.id, requirements, costThreshold });
|
|
1015
|
+
return record.id;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// Standalone fallback — direct fork
|
|
1019
|
+
const workerPath = path.join(__dirname, '../workers/sponsor-call-worker.js');
|
|
1020
|
+
const child = fork(workerPath, [], {
|
|
1021
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
1022
|
+
env: { ...process.env },
|
|
1023
|
+
});
|
|
1024
|
+
child.stdout?.on('data', d => process.stdout.write(d));
|
|
1025
|
+
child.stderr?.on('data', d => process.stderr.write(d));
|
|
1026
|
+
|
|
1027
|
+
registry.attach(record.id, child);
|
|
1028
|
+
this._activeChild = child;
|
|
1029
|
+
child.send({ type: 'init', requirements, costThreshold });
|
|
1030
|
+
|
|
1031
|
+
child.on('message', (msg) => this._dispatchWorkerMessage(msg, record, registry));
|
|
1032
|
+
|
|
1033
|
+
child.on('exit', (code) => {
|
|
1034
|
+
this._activeChild = null;
|
|
1035
|
+
this._activeProcessId = null;
|
|
1036
|
+
if (this._runningType === 'sponsor-call' && this.state.status === 'running') {
|
|
1037
|
+
registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
|
|
1038
|
+
this.state.status = 'error';
|
|
1039
|
+
this.state.error = `Worker exited unexpectedly (code ${code})`;
|
|
1040
|
+
this.websocket?.broadcastCeremonyError(this.state.error);
|
|
1041
|
+
}
|
|
1042
|
+
this._runningType = null;
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
return record.id;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// ── Legacy in-process ceremony execution (kept for backward compat) ──────────
|
|
1049
|
+
|
|
1050
|
+
async run(requirements) {
|
|
1051
|
+
if (this.state.status === 'running') {
|
|
1052
|
+
throw new Error('Ceremony already running');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
this._paused = false;
|
|
1056
|
+
this._cancelled = false;
|
|
1057
|
+
this._runningType = 'sponsor-call';
|
|
1058
|
+
this.state = { status: 'running', progress: [], result: null, error: null };
|
|
1059
|
+
|
|
1060
|
+
// Fire-and-forget: caller gets {started:true} immediately
|
|
1061
|
+
this._runAsync(requirements);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
async runSprintPlanning() {
|
|
1065
|
+
if (this.state.status === 'running') {
|
|
1066
|
+
throw new Error('Ceremony already running');
|
|
1067
|
+
}
|
|
1068
|
+
const projectDir = path.join(this.projectRoot, '.avc', 'project');
|
|
1069
|
+
this._preRunSnapshot = fs.existsSync(projectDir) ? fs.readdirSync(projectDir) : [];
|
|
1070
|
+
this._paused = false;
|
|
1071
|
+
this._cancelled = false;
|
|
1072
|
+
this._runningType = 'sprint-planning';
|
|
1073
|
+
this.state = { status: 'running', progress: [], result: null, error: null };
|
|
1074
|
+
this._runSprintPlanningAsync(); // fire-and-forget
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async _runSprintPlanningAsync() {
|
|
1078
|
+
const log = new KanbanLogger('sprint-planning-run', this.projectRoot);
|
|
1079
|
+
log.info('_runSprintPlanningAsync() started');
|
|
1080
|
+
try {
|
|
1081
|
+
const { ProjectInitiator } = await import('../../../cli/init.js');
|
|
1082
|
+
const initiator = new ProjectInitiator();
|
|
1083
|
+
const progressCallback = async (msg, substep, meta) => {
|
|
1084
|
+
if (this._cancelled) throw new Error('CEREMONY_CANCELLED');
|
|
1085
|
+
while (this._paused) {
|
|
1086
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1087
|
+
if (this._cancelled) throw new Error('CEREMONY_CANCELLED');
|
|
1088
|
+
}
|
|
1089
|
+
if (msg) {
|
|
1090
|
+
log.info(`[progress] ${msg}`);
|
|
1091
|
+
this.state.progress.push({ type: 'progress', message: msg });
|
|
1092
|
+
this.websocket?.broadcastSprintPlanningProgress(msg);
|
|
1093
|
+
}
|
|
1094
|
+
if (substep) {
|
|
1095
|
+
log.debug(`[substep] ${substep}`);
|
|
1096
|
+
this.state.progress.push({ type: 'substep', substep, meta: meta || {} });
|
|
1097
|
+
this.websocket?.broadcastSprintPlanningSubstep(substep, meta || {});
|
|
1098
|
+
}
|
|
1099
|
+
if (meta?.detail) {
|
|
1100
|
+
log.debug(`[detail] ${meta.detail}`);
|
|
1101
|
+
this.state.progress.push({ type: 'detail', detail: meta.detail });
|
|
1102
|
+
this.websocket?.broadcastSprintPlanningDetail(meta.detail);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
const result = await initiator.sprintPlanningWithCallback(progressCallback);
|
|
1106
|
+
this.state.status = 'complete';
|
|
1107
|
+
this.state.result = result;
|
|
1108
|
+
log.info('_runSprintPlanningAsync() completed', result);
|
|
1109
|
+
log.finish(true);
|
|
1110
|
+
this.websocket?.broadcastSprintPlanningComplete(result);
|
|
1111
|
+
this.websocket?.broadcastRefresh();
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
if (err.message === 'CEREMONY_CANCELLED') {
|
|
1114
|
+
this._cleanupCancelledSprintPlanning();
|
|
1115
|
+
this.state.status = 'idle';
|
|
1116
|
+
log.info('_runSprintPlanningAsync() cancelled by user');
|
|
1117
|
+
log.finish(true, 'cancelled');
|
|
1118
|
+
this.websocket?.broadcastSprintPlanningCancelled();
|
|
1119
|
+
} else {
|
|
1120
|
+
this.state.status = 'error';
|
|
1121
|
+
this.state.error = err.message;
|
|
1122
|
+
log.error('_runSprintPlanningAsync() failed', { message: err.message, stack: err.stack });
|
|
1123
|
+
log.finish(false, err.message);
|
|
1124
|
+
this.websocket?.broadcastSprintPlanningError(err.message);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async _runAsync(requirements) {
|
|
1130
|
+
const log = new KanbanLogger('ceremony-run', this.projectRoot);
|
|
1131
|
+
log.info('_runAsync() started', {
|
|
1132
|
+
requirementKeys: Object.keys(requirements || {}),
|
|
1133
|
+
missionLength: requirements?.MISSION_STATEMENT?.length,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
try {
|
|
1137
|
+
const { ProjectInitiator } = await import('../../../cli/init.js');
|
|
1138
|
+
const initiator = new ProjectInitiator();
|
|
1139
|
+
log.debug('ProjectInitiator created');
|
|
1140
|
+
|
|
1141
|
+
const progressCallback = async (msg, substep, meta) => {
|
|
1142
|
+
if (this._cancelled) throw new Error('CEREMONY_CANCELLED');
|
|
1143
|
+
while (this._paused) {
|
|
1144
|
+
await new Promise(r => setTimeout(r, 200));
|
|
1145
|
+
if (this._cancelled) throw new Error('CEREMONY_CANCELLED');
|
|
1146
|
+
}
|
|
1147
|
+
if (msg) {
|
|
1148
|
+
log.info(`[progress] ${msg}`);
|
|
1149
|
+
this.state.progress.push({ type: 'progress', message: msg });
|
|
1150
|
+
this.websocket?.broadcastCeremonyProgress(msg);
|
|
1151
|
+
}
|
|
1152
|
+
if (substep) {
|
|
1153
|
+
log.debug(`[substep] ${substep}`, meta);
|
|
1154
|
+
this.state.progress.push({ type: 'substep', substep, meta: meta || {} });
|
|
1155
|
+
this.websocket?.broadcastCeremonySubstep(substep, meta || {});
|
|
1156
|
+
}
|
|
1157
|
+
if (meta?.detail) {
|
|
1158
|
+
log.debug(`[detail] ${meta.detail}`);
|
|
1159
|
+
this.state.progress.push({ type: 'detail', detail: meta.detail });
|
|
1160
|
+
this.websocket?.broadcastCeremonyDetail(meta.detail);
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const result = await initiator.sponsorCallWithAnswers(requirements, progressCallback);
|
|
1165
|
+
|
|
1166
|
+
this.state.status = 'complete';
|
|
1167
|
+
this.state.result = result;
|
|
1168
|
+
log.info('_runAsync() completed successfully', {
|
|
1169
|
+
resultKeys: Object.keys(result || {}),
|
|
1170
|
+
});
|
|
1171
|
+
log.finish(true);
|
|
1172
|
+
|
|
1173
|
+
this.websocket?.broadcastCeremonyComplete(result);
|
|
1174
|
+
this.websocket?.broadcastRefresh();
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
if (err.message === 'CEREMONY_CANCELLED') {
|
|
1177
|
+
this.state.status = 'idle';
|
|
1178
|
+
log.info('_runAsync() cancelled by user');
|
|
1179
|
+
log.finish(true, 'cancelled');
|
|
1180
|
+
this.websocket?.broadcastCeremonyCancelled();
|
|
1181
|
+
} else {
|
|
1182
|
+
this.state.status = 'error';
|
|
1183
|
+
this.state.error = err.message;
|
|
1184
|
+
log.error('_runAsync() failed', { message: err.message, stack: err.stack });
|
|
1185
|
+
log.finish(false, err.message);
|
|
1186
|
+
this.websocket?.broadcastCeremonyError(err.message);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|