@agile-vibe-coding/avc 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/agents/agent-selector.md +23 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +318 -39
- package/cli/agents/mission-scope-generator.md +68 -4
- package/cli/agents/mission-scope-validator.md +40 -6
- package/cli/agents/project-context-extractor.md +21 -6
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/validator-documentation.json +31 -0
- package/cli/agents/validator-documentation.md +3 -1
- package/cli/api-reference-tool.js +368 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/epic-story-validator.js +284 -799
- package/cli/index.js +0 -0
- package/cli/init-model-config.js +17 -10
- package/cli/init.js +514 -92
- package/cli/kanban-server-manager.js +1 -2
- package/cli/llm-claude.js +98 -31
- package/cli/llm-gemini.js +29 -5
- package/cli/llm-local.js +493 -0
- package/cli/llm-openai.js +262 -41
- package/cli/llm-provider.js +147 -8
- package/cli/llm-token-limits.js +113 -4
- package/cli/llm-verifier.js +209 -1
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +3 -12
- package/cli/messaging-api.js +6 -12
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +23 -0
- package/cli/model-selector.js +3 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +106 -346
- package/cli/repl-old.js +1 -2
- package/cli/seed-processor.js +194 -24
- package/cli/sprint-planning-processor.js +2638 -289
- package/cli/template-processor.js +50 -3
- package/cli/token-tracker.js +50 -23
- package/cli/tools/generate-story-validators.js +1 -1
- package/cli/validation-router.js +70 -8
- package/cli/worktree-runner.js +654 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +2 -2
- package/kanban/client/src/App.jsx +43 -14
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
- package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
- package/kanban/client/src/components/stats/CostModal.jsx +34 -3
- package/kanban/client/src/hooks/useGrouping.js +59 -0
- package/kanban/client/src/lib/api.js +118 -4
- package/kanban/client/src/lib/status-grouping.js +10 -0
- package/kanban/client/src/store/kanbanStore.js +8 -0
- package/kanban/server/index.js +23 -2
- package/kanban/server/routes/ceremony.js +153 -4
- package/kanban/server/routes/costs.js +9 -3
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/settings.js +447 -14
- package/kanban/server/routes/websocket.js +7 -2
- package/kanban/server/routes/work-items.js +141 -1
- package/kanban/server/services/CeremonyService.js +275 -24
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +14 -6
- package/kanban/server/workers/sprint-planning-worker.js +94 -12
- package/package.json +2 -3
- package/cli/agents/solver-epic-api.json +0 -15
- package/cli/agents/solver-epic-api.md +0 -39
- package/cli/agents/solver-epic-backend.json +0 -15
- package/cli/agents/solver-epic-backend.md +0 -39
- package/cli/agents/solver-epic-cloud.json +0 -15
- package/cli/agents/solver-epic-cloud.md +0 -39
- package/cli/agents/solver-epic-data.json +0 -15
- package/cli/agents/solver-epic-data.md +0 -39
- package/cli/agents/solver-epic-database.json +0 -15
- package/cli/agents/solver-epic-database.md +0 -39
- package/cli/agents/solver-epic-developer.json +0 -15
- package/cli/agents/solver-epic-developer.md +0 -39
- package/cli/agents/solver-epic-devops.json +0 -15
- package/cli/agents/solver-epic-devops.md +0 -39
- package/cli/agents/solver-epic-frontend.json +0 -15
- package/cli/agents/solver-epic-frontend.md +0 -39
- package/cli/agents/solver-epic-mobile.json +0 -15
- package/cli/agents/solver-epic-mobile.md +0 -39
- package/cli/agents/solver-epic-qa.json +0 -15
- package/cli/agents/solver-epic-qa.md +0 -39
- package/cli/agents/solver-epic-security.json +0 -15
- package/cli/agents/solver-epic-security.md +0 -39
- package/cli/agents/solver-epic-solution-architect.json +0 -15
- package/cli/agents/solver-epic-solution-architect.md +0 -39
- package/cli/agents/solver-epic-test-architect.json +0 -15
- package/cli/agents/solver-epic-test-architect.md +0 -39
- package/cli/agents/solver-epic-ui.json +0 -15
- package/cli/agents/solver-epic-ui.md +0 -39
- package/cli/agents/solver-epic-ux.json +0 -15
- package/cli/agents/solver-epic-ux.md +0 -39
- package/cli/agents/solver-story-api.json +0 -15
- package/cli/agents/solver-story-api.md +0 -39
- package/cli/agents/solver-story-backend.json +0 -15
- package/cli/agents/solver-story-backend.md +0 -39
- package/cli/agents/solver-story-cloud.json +0 -15
- package/cli/agents/solver-story-cloud.md +0 -39
- package/cli/agents/solver-story-data.json +0 -15
- package/cli/agents/solver-story-data.md +0 -39
- package/cli/agents/solver-story-database.json +0 -15
- package/cli/agents/solver-story-database.md +0 -39
- package/cli/agents/solver-story-developer.json +0 -15
- package/cli/agents/solver-story-developer.md +0 -39
- package/cli/agents/solver-story-devops.json +0 -15
- package/cli/agents/solver-story-devops.md +0 -39
- package/cli/agents/solver-story-frontend.json +0 -15
- package/cli/agents/solver-story-frontend.md +0 -39
- package/cli/agents/solver-story-mobile.json +0 -15
- package/cli/agents/solver-story-mobile.md +0 -39
- package/cli/agents/solver-story-qa.json +0 -15
- package/cli/agents/solver-story-qa.md +0 -39
- package/cli/agents/solver-story-security.json +0 -15
- package/cli/agents/solver-story-security.md +0 -39
- package/cli/agents/solver-story-solution-architect.json +0 -15
- package/cli/agents/solver-story-solution-architect.md +0 -39
- package/cli/agents/solver-story-test-architect.json +0 -15
- package/cli/agents/solver-story-test-architect.md +0 -39
- package/cli/agents/solver-story-ui.json +0 -15
- package/cli/agents/solver-story-ui.md +0 -39
- package/cli/agents/solver-story-ux.json +0 -15
- package/cli/agents/solver-story-ux.md +0 -39
- package/cli/agents/validator-epic-api.json +0 -93
- package/cli/agents/validator-epic-api.md +0 -137
- package/cli/agents/validator-epic-backend.json +0 -93
- package/cli/agents/validator-epic-backend.md +0 -130
- package/cli/agents/validator-epic-cloud.json +0 -93
- package/cli/agents/validator-epic-cloud.md +0 -137
- package/cli/agents/validator-epic-data.json +0 -93
- package/cli/agents/validator-epic-data.md +0 -130
- package/cli/agents/validator-epic-database.json +0 -93
- package/cli/agents/validator-epic-database.md +0 -137
- package/cli/agents/validator-epic-developer.json +0 -74
- package/cli/agents/validator-epic-developer.md +0 -153
- package/cli/agents/validator-epic-devops.json +0 -74
- package/cli/agents/validator-epic-devops.md +0 -153
- package/cli/agents/validator-epic-frontend.json +0 -74
- package/cli/agents/validator-epic-frontend.md +0 -153
- package/cli/agents/validator-epic-mobile.json +0 -93
- package/cli/agents/validator-epic-mobile.md +0 -130
- package/cli/agents/validator-epic-qa.json +0 -93
- package/cli/agents/validator-epic-qa.md +0 -130
- package/cli/agents/validator-epic-security.json +0 -74
- package/cli/agents/validator-epic-security.md +0 -154
- package/cli/agents/validator-epic-solution-architect.json +0 -74
- package/cli/agents/validator-epic-solution-architect.md +0 -156
- package/cli/agents/validator-epic-test-architect.json +0 -93
- package/cli/agents/validator-epic-test-architect.md +0 -130
- package/cli/agents/validator-epic-ui.json +0 -93
- package/cli/agents/validator-epic-ui.md +0 -130
- package/cli/agents/validator-epic-ux.json +0 -93
- package/cli/agents/validator-epic-ux.md +0 -130
- package/cli/agents/validator-story-api.json +0 -104
- package/cli/agents/validator-story-api.md +0 -152
- package/cli/agents/validator-story-backend.json +0 -104
- package/cli/agents/validator-story-backend.md +0 -152
- package/cli/agents/validator-story-cloud.json +0 -104
- package/cli/agents/validator-story-cloud.md +0 -152
- package/cli/agents/validator-story-data.json +0 -104
- package/cli/agents/validator-story-data.md +0 -152
- package/cli/agents/validator-story-database.json +0 -104
- package/cli/agents/validator-story-database.md +0 -152
- package/cli/agents/validator-story-developer.json +0 -104
- package/cli/agents/validator-story-developer.md +0 -152
- package/cli/agents/validator-story-devops.json +0 -104
- package/cli/agents/validator-story-devops.md +0 -152
- package/cli/agents/validator-story-frontend.json +0 -104
- package/cli/agents/validator-story-frontend.md +0 -152
- package/cli/agents/validator-story-mobile.json +0 -104
- package/cli/agents/validator-story-mobile.md +0 -152
- package/cli/agents/validator-story-qa.json +0 -104
- package/cli/agents/validator-story-qa.md +0 -152
- package/cli/agents/validator-story-security.json +0 -104
- package/cli/agents/validator-story-security.md +0 -152
- package/cli/agents/validator-story-solution-architect.json +0 -104
- package/cli/agents/validator-story-solution-architect.md +0 -152
- package/cli/agents/validator-story-test-architect.json +0 -104
- package/cli/agents/validator-story-test-architect.md +0 -152
- package/cli/agents/validator-story-ui.json +0 -104
- package/cli/agents/validator-story-ui.md +0 -152
- package/cli/agents/validator-story-ux.json +0 -104
- package/cli/agents/validator-story-ux.md +0 -152
- package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
- package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
|
@@ -5,14 +5,35 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { KanbanLogger } from '../utils/kanban-logger.js';
|
|
6
6
|
import { TokenTracker } from '../../../cli/token-tracker.js';
|
|
7
7
|
import { loadAgent } from '../../../cli/agent-loader.js';
|
|
8
|
+
import { PromptLogger } from '../../../cli/prompt-logger.js';
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
10
11
|
const PROVIDER_KEY_MAP = {
|
|
11
12
|
claude: 'ANTHROPIC_API_KEY',
|
|
12
13
|
gemini: 'GEMINI_API_KEY',
|
|
13
14
|
openai: 'OPENAI_API_KEY',
|
|
15
|
+
xiaomi: 'XIAOMI_API_KEY',
|
|
14
16
|
};
|
|
15
17
|
|
|
18
|
+
/** Returns true when the provider is OpenAI and OAuth mode is active (flat-rate — no per-token billing). */
|
|
19
|
+
function isOAuthProvider(provider) {
|
|
20
|
+
return provider === 'openai' && process.env.OPENAI_AUTH_MODE === 'oauth';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Returns true if a provider has any valid auth credential (API key or token/oauth)
|
|
24
|
+
function hasProviderAuth(provider, projectRoot) {
|
|
25
|
+
if (provider === 'local') return true; // No API key needed for local models
|
|
26
|
+
if (provider === 'claude') {
|
|
27
|
+
return !!process.env.ANTHROPIC_API_KEY;
|
|
28
|
+
}
|
|
29
|
+
if (provider === 'openai') {
|
|
30
|
+
const oauthFile = path.join(projectRoot, '.avc', 'openai-oauth.json');
|
|
31
|
+
return !!(process.env.OPENAI_API_KEY ||
|
|
32
|
+
(process.env.OPENAI_AUTH_MODE === 'oauth' && fs.existsSync(oauthFile)));
|
|
33
|
+
}
|
|
34
|
+
return !!process.env[PROVIDER_KEY_MAP[provider]];
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
/**
|
|
17
38
|
* CeremonyService
|
|
18
39
|
* Orchestrates the sponsor-call ceremony from the web UI.
|
|
@@ -22,7 +43,7 @@ const PROVIDER_KEY_MAP = {
|
|
|
22
43
|
export class CeremonyService {
|
|
23
44
|
constructor(projectRoot) {
|
|
24
45
|
this.projectRoot = projectRoot;
|
|
25
|
-
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
|
|
46
|
+
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, quotaLimitInfo: null, decomposedHierarchy: null };
|
|
26
47
|
this.websocket = null;
|
|
27
48
|
this._paused = false;
|
|
28
49
|
this._cancelled = false;
|
|
@@ -54,10 +75,32 @@ export class CeremonyService {
|
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
cancel() {
|
|
78
|
+
cancel({ keepItems = false } = {}) {
|
|
58
79
|
this._cancelled = true;
|
|
80
|
+
this._keepItemsOnCancel = keepItems;
|
|
81
|
+
// Mark state as cancelling so handleWorkerExit can distinguish cancel-in-progress
|
|
82
|
+
// from a genuine unexpected crash (the worker may exit before sending 'cancelled' IPC).
|
|
83
|
+
this.state.status = 'cancelling';
|
|
59
84
|
if (this._activeChild) {
|
|
60
85
|
try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
|
|
86
|
+
// Safety net: if the worker doesn't exit within 10s (stuck in a long LLM call),
|
|
87
|
+
// send SIGTERM to force it down. handleWorkerExit will clean up.
|
|
88
|
+
const child = this._activeChild;
|
|
89
|
+
this._cancelKillTimer = setTimeout(() => {
|
|
90
|
+
this._cancelKillTimer = null;
|
|
91
|
+
if (child === this._activeChild && this.state.status === 'cancelling') {
|
|
92
|
+
console.log('[ceremony] cancel timeout — sending SIGTERM to worker');
|
|
93
|
+
try { child.kill('SIGTERM'); } catch (_) {}
|
|
94
|
+
// Last resort: SIGKILL after 3 more seconds
|
|
95
|
+
this._cancelKillTimer = setTimeout(() => {
|
|
96
|
+
this._cancelKillTimer = null;
|
|
97
|
+
if (child === this._activeChild) {
|
|
98
|
+
console.log('[ceremony] cancel SIGTERM timeout — sending SIGKILL');
|
|
99
|
+
try { child.kill('SIGKILL'); } catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
}, 3000);
|
|
102
|
+
}
|
|
103
|
+
}, 10000);
|
|
61
104
|
}
|
|
62
105
|
const isSprintPlanning = this._runningType === 'sprint-planning';
|
|
63
106
|
const msg = 'Waiting for current LLM call to finish…';
|
|
@@ -69,6 +112,7 @@ export class CeremonyService {
|
|
|
69
112
|
forceReset() {
|
|
70
113
|
this._cancelled = true;
|
|
71
114
|
this._paused = false;
|
|
115
|
+
if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
|
|
72
116
|
if (this._activeChild) {
|
|
73
117
|
try { this._activeChild.send({ type: 'cancel' }); } catch (_) {}
|
|
74
118
|
const child = this._activeChild;
|
|
@@ -78,7 +122,7 @@ export class CeremonyService {
|
|
|
78
122
|
const wasRunningType = this._runningType;
|
|
79
123
|
this._runningType = null;
|
|
80
124
|
this._activeProcessId = null;
|
|
81
|
-
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, decomposedHierarchy: null };
|
|
125
|
+
this.state = { status: 'idle', progress: [], result: null, error: null, costLimitInfo: null, quotaLimitInfo: null, decomposedHierarchy: null };
|
|
82
126
|
// Broadcast to whichever ceremony was running (or both if unknown)
|
|
83
127
|
if (wasRunningType === 'sprint-planning' || !wasRunningType) {
|
|
84
128
|
this.websocket?.broadcastSprintPlanningCancelled();
|
|
@@ -90,13 +134,20 @@ export class CeremonyService {
|
|
|
90
134
|
|
|
91
135
|
_cleanupCancelledSprintPlanning() {
|
|
92
136
|
const projectDir = path.join(this.projectRoot, '.avc', 'project');
|
|
93
|
-
if (!fs.existsSync(projectDir))
|
|
137
|
+
if (!fs.existsSync(projectDir)) {
|
|
138
|
+
console.log('[ceremony] cleanup: project dir does not exist, nothing to delete');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
94
141
|
const current = fs.readdirSync(projectDir);
|
|
95
142
|
const toDelete = current.filter(d => !this._preRunSnapshot.includes(d));
|
|
143
|
+
console.log(`[ceremony] cleanup: snapshot=${this._preRunSnapshot?.length ?? 'null'} entries, current=${current.length}, toDelete=${toDelete.length}`);
|
|
96
144
|
for (const d of toDelete) {
|
|
97
145
|
try {
|
|
98
146
|
fs.rmSync(path.join(projectDir, d), { recursive: true, force: true });
|
|
99
|
-
|
|
147
|
+
console.log(`[ceremony] cleanup: deleted ${d}`);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(`[ceremony] cleanup: failed to delete ${d}: ${err.message}`);
|
|
150
|
+
}
|
|
100
151
|
}
|
|
101
152
|
}
|
|
102
153
|
|
|
@@ -117,13 +168,14 @@ export class CeremonyService {
|
|
|
117
168
|
result: this.state.result,
|
|
118
169
|
error: this.state.error,
|
|
119
170
|
costLimitInfo: this.state.costLimitInfo || null,
|
|
171
|
+
quotaLimitInfo: this.state.quotaLimitInfo || null,
|
|
120
172
|
decomposedHierarchy: this.state.decomposedHierarchy || null,
|
|
121
173
|
};
|
|
122
174
|
}
|
|
123
175
|
|
|
124
176
|
async getAvailableModels() {
|
|
125
177
|
const { default: dotenv } = await import('dotenv');
|
|
126
|
-
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
178
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
127
179
|
|
|
128
180
|
const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
129
181
|
const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
|
|
@@ -133,7 +185,7 @@ export class CeremonyService {
|
|
|
133
185
|
modelId,
|
|
134
186
|
displayName: info.displayName,
|
|
135
187
|
provider: info.provider,
|
|
136
|
-
hasApiKey:
|
|
188
|
+
hasApiKey: hasProviderAuth(info.provider, this.projectRoot),
|
|
137
189
|
}));
|
|
138
190
|
}
|
|
139
191
|
|
|
@@ -147,7 +199,7 @@ export class CeremonyService {
|
|
|
147
199
|
|
|
148
200
|
try {
|
|
149
201
|
const { default: dotenv } = await import('dotenv');
|
|
150
|
-
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
202
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
151
203
|
log.debug('dotenv loaded', { envFile: path.join(this.projectRoot, '.env') });
|
|
152
204
|
|
|
153
205
|
// Read validation settings exclusively from avc.json
|
|
@@ -177,6 +229,11 @@ export class CeremonyService {
|
|
|
177
229
|
validator: `${validatorProvider}/${validatorModelId}`,
|
|
178
230
|
});
|
|
179
231
|
|
|
232
|
+
// Attach prompt logger
|
|
233
|
+
const _missionPromptLogger = new PromptLogger(this.projectRoot, 'sponsor-call');
|
|
234
|
+
generatorLLM.setPromptLogger(_missionPromptLogger, 'mission-generate');
|
|
235
|
+
validatorLLM.setPromptLogger(_missionPromptLogger, 'mission-validate');
|
|
236
|
+
|
|
180
237
|
// Load agent files
|
|
181
238
|
log.debug('Loading agent files');
|
|
182
239
|
const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
|
|
@@ -343,6 +400,7 @@ export class CeremonyService {
|
|
|
343
400
|
output: genUsage.outputTokens,
|
|
344
401
|
provider,
|
|
345
402
|
model: modelId,
|
|
403
|
+
skipCost: isOAuthProvider(provider),
|
|
346
404
|
});
|
|
347
405
|
}
|
|
348
406
|
const valUsage = validatorLLM.getTokenUsage();
|
|
@@ -352,6 +410,7 @@ export class CeremonyService {
|
|
|
352
410
|
output: valUsage.outputTokens,
|
|
353
411
|
provider: validatorProvider,
|
|
354
412
|
model: validatorModelId,
|
|
413
|
+
skipCost: isOAuthProvider(validatorProvider),
|
|
355
414
|
});
|
|
356
415
|
}
|
|
357
416
|
} catch (trackErr) {
|
|
@@ -381,7 +440,7 @@ export class CeremonyService {
|
|
|
381
440
|
|
|
382
441
|
try {
|
|
383
442
|
const { default: dotenv } = await import('dotenv');
|
|
384
|
-
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
443
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
385
444
|
|
|
386
445
|
const avcJsonPath = path.join(this.projectRoot, '.avc', 'avc.json');
|
|
387
446
|
const avcConfig = JSON.parse(fs.readFileSync(avcJsonPath, 'utf8'));
|
|
@@ -400,6 +459,11 @@ export class CeremonyService {
|
|
|
400
459
|
const generatorLLM = await LLMProvider.create(provider, modelId);
|
|
401
460
|
const validatorLLM = await LLMProvider.create(validatorProvider, validatorModelId);
|
|
402
461
|
|
|
462
|
+
// Attach prompt logger
|
|
463
|
+
const _refinePromptLogger = new PromptLogger(this.projectRoot, 'sponsor-call');
|
|
464
|
+
generatorLLM.setPromptLogger(_refinePromptLogger, 'mission-refine-generate');
|
|
465
|
+
validatorLLM.setPromptLogger(_refinePromptLogger, 'mission-refine-validate');
|
|
466
|
+
|
|
403
467
|
const generatorAgent = loadAgent('mission-scope-generator.md', this.projectRoot);
|
|
404
468
|
const validatorAgent = loadAgent('mission-scope-validator.md', this.projectRoot);
|
|
405
469
|
|
|
@@ -535,6 +599,7 @@ export class CeremonyService {
|
|
|
535
599
|
output: genUsage.outputTokens,
|
|
536
600
|
provider,
|
|
537
601
|
model: modelId,
|
|
602
|
+
skipCost: isOAuthProvider(provider),
|
|
538
603
|
});
|
|
539
604
|
}
|
|
540
605
|
const valUsage = validatorLLM.getTokenUsage();
|
|
@@ -544,6 +609,7 @@ export class CeremonyService {
|
|
|
544
609
|
output: valUsage.outputTokens,
|
|
545
610
|
provider: validatorProvider,
|
|
546
611
|
model: validatorModelId,
|
|
612
|
+
skipCost: isOAuthProvider(validatorProvider),
|
|
547
613
|
});
|
|
548
614
|
}
|
|
549
615
|
} catch (trackErr) {
|
|
@@ -562,10 +628,11 @@ export class CeremonyService {
|
|
|
562
628
|
async generateCustomArchitecture(description, modelId, provider) {
|
|
563
629
|
const log = new KanbanLogger('arch-custom', this.projectRoot);
|
|
564
630
|
const { default: dotenv } = await import('dotenv');
|
|
565
|
-
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
631
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
566
632
|
|
|
567
633
|
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
568
634
|
const llm = await LLMProvider.create(provider, modelId);
|
|
635
|
+
llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'arch-generate');
|
|
569
636
|
const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
|
|
570
637
|
|
|
571
638
|
const prompt =
|
|
@@ -588,10 +655,11 @@ export class CeremonyService {
|
|
|
588
655
|
async refineCustomArchitecture(currentArch, refinementRequest, modelId, provider) {
|
|
589
656
|
const log = new KanbanLogger('arch-custom', this.projectRoot);
|
|
590
657
|
const { default: dotenv } = await import('dotenv');
|
|
591
|
-
dotenv.config({ path: path.join(this.projectRoot, '.env') });
|
|
658
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
592
659
|
|
|
593
660
|
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
594
661
|
const llm = await LLMProvider.create(provider, modelId);
|
|
662
|
+
llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'arch-refine');
|
|
595
663
|
const agentInstruction = loadAgent('architecture-recommender.md', this.projectRoot);
|
|
596
664
|
|
|
597
665
|
const prompt =
|
|
@@ -636,6 +704,7 @@ export class CeremonyService {
|
|
|
636
704
|
output: usage.outputTokens,
|
|
637
705
|
provider: usage.provider,
|
|
638
706
|
model: usage.model,
|
|
707
|
+
skipCost: isOAuthProvider(usage.provider),
|
|
639
708
|
});
|
|
640
709
|
}
|
|
641
710
|
}
|
|
@@ -682,6 +751,7 @@ export class CeremonyService {
|
|
|
682
751
|
output: usage.outputTokens,
|
|
683
752
|
provider: usage.provider,
|
|
684
753
|
model: usage.model,
|
|
754
|
+
skipCost: isOAuthProvider(usage.provider),
|
|
685
755
|
});
|
|
686
756
|
}
|
|
687
757
|
}
|
|
@@ -730,6 +800,7 @@ export class CeremonyService {
|
|
|
730
800
|
output: usage.outputTokens,
|
|
731
801
|
provider: usage.provider,
|
|
732
802
|
model: usage.model,
|
|
803
|
+
skipCost: isOAuthProvider(usage.provider),
|
|
733
804
|
});
|
|
734
805
|
}
|
|
735
806
|
}
|
|
@@ -748,6 +819,71 @@ export class CeremonyService {
|
|
|
748
819
|
}
|
|
749
820
|
}
|
|
750
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Refine a single requirement field using an LLM.
|
|
824
|
+
* @param {string} fieldKey - e.g. 'TARGET_USERS'
|
|
825
|
+
* @param {string} fieldLabel - e.g. 'Target Users'
|
|
826
|
+
* @param {string} currentValue - current field content
|
|
827
|
+
* @param {string} refinementRequest - user's instruction for improvement
|
|
828
|
+
* @param {object} context - { mission, scope } for grounding
|
|
829
|
+
* @param {string} modelId
|
|
830
|
+
* @param {string} provider
|
|
831
|
+
* @returns {Promise<{ value: string }>}
|
|
832
|
+
*/
|
|
833
|
+
async refineField(fieldKey, fieldLabel, currentValue, refinementRequest, context, modelId, provider) {
|
|
834
|
+
const log = new KanbanLogger('refine-field', this.projectRoot);
|
|
835
|
+
log.info('refineField() called', { fieldKey, modelId, provider });
|
|
836
|
+
|
|
837
|
+
const { default: dotenv } = await import('dotenv');
|
|
838
|
+
dotenv.config({ path: path.join(this.projectRoot, '.env'), override: true });
|
|
839
|
+
|
|
840
|
+
const { LLMProvider } = await import('../../../cli/llm-provider.js');
|
|
841
|
+
const llm = await LLMProvider.create(provider, modelId);
|
|
842
|
+
llm.setPromptLogger(new PromptLogger(this.projectRoot, 'sponsor-call'), 'refine-field');
|
|
843
|
+
|
|
844
|
+
const prompt =
|
|
845
|
+
`You are helping refine a single field of a software project's requirements.\n\n` +
|
|
846
|
+
`Project Mission: ${context.mission}\n\n` +
|
|
847
|
+
`Project Scope: ${context.scope}\n\n` +
|
|
848
|
+
`Field: "${fieldLabel}" (key: ${fieldKey})\n` +
|
|
849
|
+
`Current value:\n${currentValue}\n\n` +
|
|
850
|
+
`User's refinement request: ${refinementRequest}\n\n` +
|
|
851
|
+
`Return a JSON object with a single key "value" containing the improved text for this field. ` +
|
|
852
|
+
`Keep the same general format and level of detail. Do not include any other keys.`;
|
|
853
|
+
|
|
854
|
+
try {
|
|
855
|
+
const result = await llm.generateJSON(prompt);
|
|
856
|
+
if (!result?.value) {
|
|
857
|
+
throw new Error('Model returned empty result');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Track token usage
|
|
861
|
+
try {
|
|
862
|
+
const tracker = new TokenTracker(path.join(this.projectRoot, '.avc'));
|
|
863
|
+
const usage = llm.getTokenUsage();
|
|
864
|
+
if (usage.totalCalls > 0) {
|
|
865
|
+
tracker.addExecution('refine-field', {
|
|
866
|
+
input: usage.inputTokens,
|
|
867
|
+
output: usage.outputTokens,
|
|
868
|
+
provider: usage.provider,
|
|
869
|
+
model: usage.model,
|
|
870
|
+
skipCost: isOAuthProvider(usage.provider),
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
} catch (trackErr) {
|
|
874
|
+
log.warn('Failed to track token usage', { error: trackErr.message });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
log.info('refineField() completed', { fieldKey, valueLength: result.value.length });
|
|
878
|
+
log.finish(true);
|
|
879
|
+
return { value: result.value };
|
|
880
|
+
} catch (err) {
|
|
881
|
+
log.error('refineField() failed', { fieldKey, message: err.message });
|
|
882
|
+
log.finish(false, err.message);
|
|
883
|
+
throw err;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
751
887
|
// ── Fork-based ceremony execution ───────────────────────────────────────────
|
|
752
888
|
|
|
753
889
|
/**
|
|
@@ -803,15 +939,25 @@ export class CeremonyService {
|
|
|
803
939
|
else this.websocket?.broadcastCeremonyComplete(msg.result);
|
|
804
940
|
this.websocket?.broadcastRefresh();
|
|
805
941
|
break;
|
|
806
|
-
case 'cancelled':
|
|
807
|
-
if (
|
|
942
|
+
case 'cancelled': {
|
|
943
|
+
if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
|
|
944
|
+
const itemsKept = isSP && this._keepItemsOnCancel;
|
|
945
|
+
if (isSP && !this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
|
|
946
|
+
// Reload + refresh regardless of keep/delete so the board reflects disk state
|
|
947
|
+
if (isSP) {
|
|
948
|
+
await this._reloadCallback?.();
|
|
949
|
+
this.websocket?.broadcastRefresh();
|
|
950
|
+
}
|
|
808
951
|
this.state.status = 'idle';
|
|
809
952
|
this._activeChild = null;
|
|
810
953
|
this._activeProcessId = null;
|
|
954
|
+
this._keepItemsOnCancel = false;
|
|
955
|
+
this._runningType = null;
|
|
811
956
|
registry.setStatus(record.id, 'cancelled');
|
|
812
|
-
if (isSP) this.websocket?.broadcastSprintPlanningCancelled();
|
|
957
|
+
if (isSP) this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
|
|
813
958
|
else this.websocket?.broadcastCeremonyCancelled();
|
|
814
959
|
break;
|
|
960
|
+
}
|
|
815
961
|
case 'error':
|
|
816
962
|
this.state.status = 'error';
|
|
817
963
|
this.state.error = msg.error;
|
|
@@ -831,6 +977,17 @@ export class CeremonyService {
|
|
|
831
977
|
this.websocket?.broadcastCostLimit(msg.cost, msg.threshold, this._runningType);
|
|
832
978
|
break;
|
|
833
979
|
}
|
|
980
|
+
case 'quota-limit':
|
|
981
|
+
this.state.status = 'quota-limit-pending';
|
|
982
|
+
this.state.quotaLimitInfo = {
|
|
983
|
+
validatorName: msg.validatorName,
|
|
984
|
+
errMsg: msg.errMsg,
|
|
985
|
+
provider: msg.provider,
|
|
986
|
+
model: msg.model,
|
|
987
|
+
};
|
|
988
|
+
registry.setStatus(record.id, 'paused');
|
|
989
|
+
this.websocket?.broadcastQuotaLimit(msg.provider, msg.model, msg.errMsg, msg.validatorName, this._runningType);
|
|
990
|
+
break;
|
|
834
991
|
case 'decomposition-complete':
|
|
835
992
|
this.state.status = 'awaiting-selection';
|
|
836
993
|
this.state.decomposedHierarchy = msg.hierarchy;
|
|
@@ -838,6 +995,22 @@ export class CeremonyService {
|
|
|
838
995
|
registry.setStatus(record.id, 'paused');
|
|
839
996
|
this.websocket?.broadcastSprintPlanningDecompositionComplete(msg.hierarchy);
|
|
840
997
|
break;
|
|
998
|
+
case 'item-written':
|
|
999
|
+
// A single epic or story work.json was written — debounce board refresh
|
|
1000
|
+
// so rapid writes don't cause excessive reloads.
|
|
1001
|
+
if (this._itemWrittenTimer) clearTimeout(this._itemWrittenTimer);
|
|
1002
|
+
this._itemWrittenTimer = setTimeout(async () => {
|
|
1003
|
+
this._itemWrittenTimer = null;
|
|
1004
|
+
await this._reloadCallback?.();
|
|
1005
|
+
this.websocket?.broadcastRefresh();
|
|
1006
|
+
}, 300);
|
|
1007
|
+
break;
|
|
1008
|
+
case 'hierarchy-written':
|
|
1009
|
+
// Stage 6 finished writing all work.json files — final refresh to catch everything.
|
|
1010
|
+
if (this._itemWrittenTimer) { clearTimeout(this._itemWrittenTimer); this._itemWrittenTimer = null; }
|
|
1011
|
+
await this._reloadCallback?.();
|
|
1012
|
+
this.websocket?.broadcastRefresh();
|
|
1013
|
+
break;
|
|
841
1014
|
}
|
|
842
1015
|
}
|
|
843
1016
|
|
|
@@ -867,6 +1040,21 @@ export class CeremonyService {
|
|
|
867
1040
|
this.state.costLimitInfo = null;
|
|
868
1041
|
}
|
|
869
1042
|
|
|
1043
|
+
/**
|
|
1044
|
+
* Resume ceremony after quota-limit pause.
|
|
1045
|
+
* newProvider/newModel: if provided, worker switches validation+solver stage config.
|
|
1046
|
+
* If null, worker retries with the same model (e.g., user added credits).
|
|
1047
|
+
*/
|
|
1048
|
+
continueAfterQuota(newProvider = null, newModel = null) {
|
|
1049
|
+
if (this._activeChild) {
|
|
1050
|
+
try {
|
|
1051
|
+
this._activeChild.send({ type: 'quota-continue', newProvider, newModel });
|
|
1052
|
+
} catch (_) {}
|
|
1053
|
+
}
|
|
1054
|
+
this.state.status = 'running';
|
|
1055
|
+
this.state.quotaLimitInfo = null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
870
1058
|
/**
|
|
871
1059
|
* Read the cost threshold for a ceremony type from avc.json.
|
|
872
1060
|
* Returns null if not configured (unlimited).
|
|
@@ -874,7 +1062,13 @@ export class CeremonyService {
|
|
|
874
1062
|
_getCostThreshold(ceremonyType) {
|
|
875
1063
|
try {
|
|
876
1064
|
const config = JSON.parse(fs.readFileSync(path.join(this.projectRoot, '.avc', 'avc.json'), 'utf8'));
|
|
877
|
-
|
|
1065
|
+
const threshold = config.settings?.costThresholds?.[ceremonyType] ?? null;
|
|
1066
|
+
if (threshold == null) return null;
|
|
1067
|
+
// OAuth providers are flat-rate (no per-token billing) — cost limits don't apply
|
|
1068
|
+
const ceremony = config.settings?.ceremonies?.find(c => c.name === ceremonyType);
|
|
1069
|
+
const provider = ceremony?.provider ?? config.settings?.provider ?? 'claude';
|
|
1070
|
+
if (isOAuthProvider(provider)) return null;
|
|
1071
|
+
return threshold;
|
|
878
1072
|
} catch { return null; }
|
|
879
1073
|
}
|
|
880
1074
|
|
|
@@ -887,20 +1081,42 @@ export class CeremonyService {
|
|
|
887
1081
|
}
|
|
888
1082
|
|
|
889
1083
|
/** Relay worker exit notification received via CLI → Kanban IPC channel. */
|
|
890
|
-
handleWorkerExit(processId, code) {
|
|
1084
|
+
async handleWorkerExit(processId, code) {
|
|
891
1085
|
const record = this._registry?.getByProcessId(processId);
|
|
892
1086
|
if (!record) return;
|
|
1087
|
+
if (this._cancelKillTimer) { clearTimeout(this._cancelKillTimer); this._cancelKillTimer = null; }
|
|
893
1088
|
this._activeChild = null;
|
|
894
1089
|
this._activeProcessId = null;
|
|
895
1090
|
const isSP = record.type === 'sprint-planning';
|
|
1091
|
+
|
|
1092
|
+
// Cancel was in progress but the worker exited before sending its 'cancelled' IPC
|
|
1093
|
+
// message (e.g. IPC channel broke, disconnect handler fired with exit code 1).
|
|
1094
|
+
// Treat this as a successful cancellation — run cleanup and broadcast.
|
|
1095
|
+
if (this.state.status === 'cancelling') {
|
|
1096
|
+
const itemsKept = isSP && this._keepItemsOnCancel;
|
|
1097
|
+
if (isSP && !this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
|
|
1098
|
+
if (isSP) {
|
|
1099
|
+
await this._reloadCallback?.();
|
|
1100
|
+
this.websocket?.broadcastRefresh();
|
|
1101
|
+
}
|
|
1102
|
+
this.state.status = 'idle';
|
|
1103
|
+
this._keepItemsOnCancel = false;
|
|
1104
|
+
this._runningType = null;
|
|
1105
|
+
this._registry.setStatus(record.id, 'cancelled');
|
|
1106
|
+
if (isSP) this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
|
|
1107
|
+
else this.websocket?.broadcastCeremonyCancelled();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
896
1111
|
const wasActive = this._runningType === record.type &&
|
|
897
|
-
(this.state.status === 'running' || this.state.status === 'cost-limit-pending');
|
|
1112
|
+
(this.state.status === 'running' || this.state.status === 'cost-limit-pending' || this.state.status === 'quota-limit-pending');
|
|
898
1113
|
if (wasActive) {
|
|
899
1114
|
const error = `Worker exited unexpectedly (code ${code})`;
|
|
900
1115
|
this._registry.setStatus(record.id, 'error', { error });
|
|
901
1116
|
this.state.status = 'error';
|
|
902
1117
|
this.state.error = error;
|
|
903
1118
|
this.state.costLimitInfo = null;
|
|
1119
|
+
this.state.quotaLimitInfo = null;
|
|
904
1120
|
if (isSP) this.websocket?.broadcastSprintPlanningError(error);
|
|
905
1121
|
else this.websocket?.broadcastCeremonyError(error);
|
|
906
1122
|
}
|
|
@@ -919,7 +1135,7 @@ export class CeremonyService {
|
|
|
919
1135
|
* @param {ProcessRegistry} registry
|
|
920
1136
|
* @returns {string} processId
|
|
921
1137
|
*/
|
|
922
|
-
async runSprintPlanningInProcess(registry) {
|
|
1138
|
+
async runSprintPlanningInProcess(registry, resumeFrom = null) {
|
|
923
1139
|
if (this.state.status === 'running') {
|
|
924
1140
|
throw new Error('Ceremony already running');
|
|
925
1141
|
}
|
|
@@ -946,7 +1162,7 @@ export class CeremonyService {
|
|
|
946
1162
|
};
|
|
947
1163
|
this._activeChild = proxy;
|
|
948
1164
|
registry.attach(record.id, proxy);
|
|
949
|
-
process.send({ type: 'ceremony:fork', ceremonyType: 'sprint-planning', processId: record.id, costThreshold });
|
|
1165
|
+
process.send({ type: 'ceremony:fork', ceremonyType: 'sprint-planning', processId: record.id, costThreshold, resumeFrom });
|
|
950
1166
|
return record.id;
|
|
951
1167
|
}
|
|
952
1168
|
|
|
@@ -954,6 +1170,7 @@ export class CeremonyService {
|
|
|
954
1170
|
const workerPath = path.join(__dirname, '../workers/sprint-planning-worker.js');
|
|
955
1171
|
const child = fork(workerPath, [], {
|
|
956
1172
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
1173
|
+
cwd: this.projectRoot,
|
|
957
1174
|
env: { ...process.env },
|
|
958
1175
|
});
|
|
959
1176
|
child.stdout?.on('data', d => process.stdout.write(d));
|
|
@@ -961,13 +1178,26 @@ export class CeremonyService {
|
|
|
961
1178
|
|
|
962
1179
|
registry.attach(record.id, child);
|
|
963
1180
|
this._activeChild = child;
|
|
964
|
-
child.send({ type: 'init', costThreshold });
|
|
1181
|
+
child.send({ type: 'init', costThreshold, resumeFrom });
|
|
965
1182
|
|
|
966
1183
|
child.on('message', (msg) => this._dispatchWorkerMessage(msg, record, registry));
|
|
967
1184
|
|
|
968
|
-
child.on('exit', (code) => {
|
|
1185
|
+
child.on('exit', async (code) => {
|
|
969
1186
|
this._activeChild = null;
|
|
970
1187
|
this._activeProcessId = null;
|
|
1188
|
+
// Cancel was in progress but worker exited before sending 'cancelled' IPC
|
|
1189
|
+
if (this.state.status === 'cancelling') {
|
|
1190
|
+
const itemsKept = this._keepItemsOnCancel;
|
|
1191
|
+
if (!this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
|
|
1192
|
+
await this._reloadCallback?.();
|
|
1193
|
+
this.websocket?.broadcastRefresh();
|
|
1194
|
+
this.state.status = 'idle';
|
|
1195
|
+
this._keepItemsOnCancel = false;
|
|
1196
|
+
this._runningType = null;
|
|
1197
|
+
registry.setStatus(record.id, 'cancelled');
|
|
1198
|
+
this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
971
1201
|
if (this._runningType === 'sprint-planning' && this.state.status === 'running') {
|
|
972
1202
|
registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
|
|
973
1203
|
this.state.status = 'error';
|
|
@@ -1019,6 +1249,7 @@ export class CeremonyService {
|
|
|
1019
1249
|
const workerPath = path.join(__dirname, '../workers/sponsor-call-worker.js');
|
|
1020
1250
|
const child = fork(workerPath, [], {
|
|
1021
1251
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
1252
|
+
cwd: this.projectRoot,
|
|
1022
1253
|
env: { ...process.env },
|
|
1023
1254
|
});
|
|
1024
1255
|
child.stdout?.on('data', d => process.stdout.write(d));
|
|
@@ -1033,6 +1264,15 @@ export class CeremonyService {
|
|
|
1033
1264
|
child.on('exit', (code) => {
|
|
1034
1265
|
this._activeChild = null;
|
|
1035
1266
|
this._activeProcessId = null;
|
|
1267
|
+
// Cancel was in progress but worker exited before sending 'cancelled' IPC
|
|
1268
|
+
if (this.state.status === 'cancelling') {
|
|
1269
|
+
this.state.status = 'idle';
|
|
1270
|
+
this._keepItemsOnCancel = false;
|
|
1271
|
+
this._runningType = null;
|
|
1272
|
+
registry.setStatus(record.id, 'cancelled');
|
|
1273
|
+
this.websocket?.broadcastCeremonyCancelled();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1036
1276
|
if (this._runningType === 'sponsor-call' && this.state.status === 'running') {
|
|
1037
1277
|
registry.setStatus(record.id, 'error', { error: `Worker exited unexpectedly (code ${code})` });
|
|
1038
1278
|
this.state.status = 'error';
|
|
@@ -1111,11 +1351,17 @@ export class CeremonyService {
|
|
|
1111
1351
|
this.websocket?.broadcastRefresh();
|
|
1112
1352
|
} catch (err) {
|
|
1113
1353
|
if (err.message === 'CEREMONY_CANCELLED') {
|
|
1114
|
-
this.
|
|
1354
|
+
const itemsKept = this._keepItemsOnCancel;
|
|
1355
|
+
if (!this._keepItemsOnCancel) this._cleanupCancelledSprintPlanning();
|
|
1356
|
+
// Reload + refresh regardless of keep/delete so the board reflects disk state
|
|
1357
|
+
await this._reloadCallback?.();
|
|
1358
|
+
this.websocket?.broadcastRefresh();
|
|
1359
|
+
this._keepItemsOnCancel = false;
|
|
1360
|
+
this._runningType = null;
|
|
1115
1361
|
this.state.status = 'idle';
|
|
1116
|
-
log.info('_runSprintPlanningAsync() cancelled by user');
|
|
1362
|
+
log.info('_runSprintPlanningAsync() cancelled by user', { itemsKept });
|
|
1117
1363
|
log.finish(true, 'cancelled');
|
|
1118
|
-
this.websocket?.broadcastSprintPlanningCancelled();
|
|
1364
|
+
this.websocket?.broadcastSprintPlanningCancelled(itemsKept);
|
|
1119
1365
|
} else {
|
|
1120
1366
|
this.state.status = 'error';
|
|
1121
1367
|
this.state.error = err.message;
|
|
@@ -1163,6 +1409,11 @@ export class CeremonyService {
|
|
|
1163
1409
|
|
|
1164
1410
|
const result = await initiator.sponsorCallWithAnswers(requirements, progressCallback);
|
|
1165
1411
|
|
|
1412
|
+
// sponsorCallWithAnswers returns { error: true, message } on validation failure instead of throwing
|
|
1413
|
+
if (result?.error === true) {
|
|
1414
|
+
throw new Error(result.message || 'Ceremony failed');
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1166
1417
|
this.state.status = 'complete';
|
|
1167
1418
|
this.state.result = result;
|
|
1168
1419
|
log.info('_runAsync() completed successfully', {
|