@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
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree-runner.js — Git worktree lifecycle for AI-assisted task implementation.
|
|
3
|
+
*
|
|
4
|
+
* Flow: createWorktree → readDocChain → generate→validate loop → runTests → commitAndMerge → cleanup
|
|
5
|
+
*
|
|
6
|
+
* Uses split agents (code-implementer + code-validator) with ceremony-grade configuration:
|
|
7
|
+
* - Per-stage provider/model from avc.json "run" ceremony
|
|
8
|
+
* - Check definitions from src/cli/checks/code/*.json (with project overrides)
|
|
9
|
+
* - Validation loop controlled by maxValidationIterations and acceptanceThreshold
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { execSync, execFileSync } from 'child_process';
|
|
15
|
+
import { LLMProvider } from './llm-provider.js';
|
|
16
|
+
import { loadAgent } from './agent-loader.js';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Execute a git command in a given directory, returning stdout.
|
|
24
|
+
* Throws on non-zero exit.
|
|
25
|
+
*/
|
|
26
|
+
function git(args, cwd) {
|
|
27
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 60_000 }).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute hierarchy prefix from a task ID.
|
|
32
|
+
* context-0001-0002-0003 → e0001_s0002_t0003
|
|
33
|
+
* context-0001-0002 → e0001_s0002
|
|
34
|
+
* context-0001 → e0001
|
|
35
|
+
*/
|
|
36
|
+
export function computeHierarchyPrefix(taskId) {
|
|
37
|
+
const parts = taskId.replace('context-', '').split('-');
|
|
38
|
+
const labels = ['e', 's', 't', 'st'];
|
|
39
|
+
return parts.slice(0, 4).map((p, i) => `${labels[i] || 'x'}${p}`).join('_');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class WorktreeRunner {
|
|
43
|
+
/**
|
|
44
|
+
* @param {string} taskId — context-XXXX-XXXX-XXXX
|
|
45
|
+
* @param {string} projectRoot — absolute path to the project (parent of .avc)
|
|
46
|
+
*/
|
|
47
|
+
constructor(taskId, projectRoot) {
|
|
48
|
+
this.taskId = taskId;
|
|
49
|
+
this.projectRoot = projectRoot;
|
|
50
|
+
this.avcPath = path.join(projectRoot, '.avc');
|
|
51
|
+
this.worktreePath = path.join(this.avcPath, 'worktrees', taskId);
|
|
52
|
+
this.branchName = `avc/${taskId}`;
|
|
53
|
+
this.prefix = computeHierarchyPrefix(taskId);
|
|
54
|
+
|
|
55
|
+
// Read ceremony config for the 'run' ceremony
|
|
56
|
+
const configPath = path.join(this.avcPath, 'avc.json');
|
|
57
|
+
let ceremony = null;
|
|
58
|
+
try {
|
|
59
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
60
|
+
ceremony = cfg.settings?.ceremonies?.find(c => c.name === 'run');
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
this._ceremony = ceremony || {};
|
|
64
|
+
this._defaultProvider = ceremony?.provider || 'local';
|
|
65
|
+
this._defaultModel = ceremony?.defaultModel || 'qwen/qwen3-coder-next';
|
|
66
|
+
this._maxIterations = ceremony?.maxValidationIterations ?? 3;
|
|
67
|
+
this._acceptanceThreshold = ceremony?.acceptanceThreshold ?? 80;
|
|
68
|
+
this._stageProviders = {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pre-flight check: verify the project has minimum infrastructure before running tasks.
|
|
73
|
+
* Warns (does not block) if package.json or test config is missing — the scaffolding
|
|
74
|
+
* epic should have created these, but if it hasn't run yet, we still proceed with warnings.
|
|
75
|
+
*/
|
|
76
|
+
_preflightCheck(progressCallback) {
|
|
77
|
+
const warnings = [];
|
|
78
|
+
|
|
79
|
+
// Check package.json
|
|
80
|
+
const pkgPath = path.join(this.projectRoot, 'package.json');
|
|
81
|
+
if (!fs.existsSync(pkgPath)) {
|
|
82
|
+
warnings.push('No package.json found — tests will be skipped. Run the Project Scaffolding epic first.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check git repo
|
|
86
|
+
try {
|
|
87
|
+
git(['rev-parse', '--git-dir'], this.projectRoot);
|
|
88
|
+
} catch {
|
|
89
|
+
warnings.push('No git repository found — worktree creation may fail. Run "git init" first.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const w of warnings) {
|
|
93
|
+
this.debug(`[PRE-FLIGHT WARNING] ${w}`);
|
|
94
|
+
progressCallback?.(`Warning: ${w}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
debug(msg, data = null) {
|
|
99
|
+
const ts = new Date().toISOString();
|
|
100
|
+
if (data) {
|
|
101
|
+
console.log(`[DEBUG][${ts}] ${msg}`, JSON.stringify(data, null, 2));
|
|
102
|
+
} else {
|
|
103
|
+
console.log(`[DEBUG][${ts}] ${msg}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Main execution — full worktree lifecycle.
|
|
109
|
+
* @param {Function} progressCallback — (message) => void
|
|
110
|
+
* @param {Function} cancelledCheck — () => boolean
|
|
111
|
+
* @returns {Promise<{ success: boolean, error?: string }>}
|
|
112
|
+
*/
|
|
113
|
+
async execute(progressCallback, cancelledCheck = null) {
|
|
114
|
+
try {
|
|
115
|
+
// 0. Pre-flight: verify project structure exists
|
|
116
|
+
this._preflightCheck(progressCallback);
|
|
117
|
+
|
|
118
|
+
// 1. Create worktree
|
|
119
|
+
progressCallback?.('Creating git worktree...');
|
|
120
|
+
if (cancelledCheck?.()) throw new Error('CANCELLED');
|
|
121
|
+
this.createWorktree();
|
|
122
|
+
|
|
123
|
+
// 2. Read full doc chain
|
|
124
|
+
progressCallback?.('Reading documentation chain...');
|
|
125
|
+
if (cancelledCheck?.()) throw new Error('CANCELLED');
|
|
126
|
+
const context = this.readDocChain();
|
|
127
|
+
|
|
128
|
+
// 3. Implement code via LLM
|
|
129
|
+
progressCallback?.('Implementing code with AI agent...');
|
|
130
|
+
if (cancelledCheck?.()) throw new Error('CANCELLED');
|
|
131
|
+
await this.implementCode(context, progressCallback);
|
|
132
|
+
|
|
133
|
+
// 4. Run tests
|
|
134
|
+
progressCallback?.('Running tests...');
|
|
135
|
+
if (cancelledCheck?.()) throw new Error('CANCELLED');
|
|
136
|
+
const testResult = this.runTests();
|
|
137
|
+
|
|
138
|
+
if (!testResult.passed) {
|
|
139
|
+
progressCallback?.(`Tests failed: ${testResult.summary}`);
|
|
140
|
+
this.cleanup();
|
|
141
|
+
return { success: false, error: `Tests failed: ${testResult.summary}` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 5. Commit and merge
|
|
145
|
+
progressCallback?.('Committing and merging to main...');
|
|
146
|
+
if (cancelledCheck?.()) throw new Error('CANCELLED');
|
|
147
|
+
this.commitAndMerge();
|
|
148
|
+
|
|
149
|
+
// 6. Cleanup
|
|
150
|
+
progressCallback?.('Cleaning up worktree...');
|
|
151
|
+
this.cleanup();
|
|
152
|
+
|
|
153
|
+
return { success: true };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
// Always cleanup on failure
|
|
156
|
+
try { this.cleanup(); } catch {}
|
|
157
|
+
|
|
158
|
+
if (err.message === 'CANCELLED') {
|
|
159
|
+
return { success: false, error: 'Cancelled' };
|
|
160
|
+
}
|
|
161
|
+
return { success: false, error: err.message };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a new git worktree for the task.
|
|
167
|
+
*/
|
|
168
|
+
createWorktree() {
|
|
169
|
+
const worktreesDir = path.dirname(this.worktreePath);
|
|
170
|
+
if (!fs.existsSync(worktreesDir)) {
|
|
171
|
+
fs.mkdirSync(worktreesDir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Ensure we're in a git repo
|
|
175
|
+
try {
|
|
176
|
+
git(['rev-parse', '--git-dir'], this.projectRoot);
|
|
177
|
+
} catch {
|
|
178
|
+
throw new Error('Not a git repository. Initialize with "git init" first.');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Ensure there's at least one commit
|
|
182
|
+
try {
|
|
183
|
+
git(['rev-parse', 'HEAD'], this.projectRoot);
|
|
184
|
+
} catch {
|
|
185
|
+
// Create initial commit if repo is empty
|
|
186
|
+
git(['commit', '--allow-empty', '-m', 'Initial commit'], this.projectRoot);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Remove stale worktree if it exists
|
|
190
|
+
if (fs.existsSync(this.worktreePath)) {
|
|
191
|
+
try { git(['worktree', 'remove', '--force', this.worktreePath], this.projectRoot); } catch {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Delete branch if it exists from a previous failed run
|
|
195
|
+
try { git(['branch', '-D', this.branchName], this.projectRoot); } catch {}
|
|
196
|
+
|
|
197
|
+
// Create new worktree with branch
|
|
198
|
+
git(['worktree', 'add', this.worktreePath, '-b', this.branchName], this.projectRoot);
|
|
199
|
+
this.debug('Worktree created', { path: this.worktreePath, branch: this.branchName });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Read the full documentation chain: project → epic → story → task (+ subtasks).
|
|
204
|
+
* Returns concatenated context string.
|
|
205
|
+
*/
|
|
206
|
+
readDocChain() {
|
|
207
|
+
const projectPath = path.join(this.avcPath, 'project');
|
|
208
|
+
const parts = [];
|
|
209
|
+
|
|
210
|
+
// Walk from task ID up through ancestors
|
|
211
|
+
// context-0001-0001-0001 → [context-0001, context-0001-0001, context-0001-0001-0001]
|
|
212
|
+
const segments = this.taskId.match(/context-\d{4}(-\d{4})*/g) || [];
|
|
213
|
+
const ancestorIds = [];
|
|
214
|
+
const idParts = this.taskId.replace('context-', '').split('-');
|
|
215
|
+
let current = 'context';
|
|
216
|
+
for (const part of idParts) {
|
|
217
|
+
current += `-${part}`;
|
|
218
|
+
ancestorIds.push(current);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Read project-level docs
|
|
222
|
+
const projectDoc = this._readIfExists(path.join(projectPath, 'doc.md'));
|
|
223
|
+
const projectContext = this._readIfExists(path.join(projectPath, 'context.md'));
|
|
224
|
+
if (projectDoc) parts.push(`# Project Documentation\n\n${projectDoc}`);
|
|
225
|
+
if (projectContext) parts.push(`# Project Context\n\n${projectContext}`);
|
|
226
|
+
|
|
227
|
+
// Read each ancestor level
|
|
228
|
+
let dirPath = projectPath;
|
|
229
|
+
for (const id of ancestorIds) {
|
|
230
|
+
dirPath = path.join(projectPath, ...ancestorIds.slice(0, ancestorIds.indexOf(id) + 1).map((_, i) => ancestorIds[i]));
|
|
231
|
+
// Actually build the nested path correctly
|
|
232
|
+
break; // Will use a different approach
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Build nested path: project/epic-id/story-id/task-id
|
|
236
|
+
const epicId = ancestorIds[0]; // context-0001
|
|
237
|
+
const dirs = [projectPath];
|
|
238
|
+
let nested = projectPath;
|
|
239
|
+
for (const id of ancestorIds) {
|
|
240
|
+
nested = path.join(nested, id);
|
|
241
|
+
dirs.push(nested);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Read context.md and doc.md at each level (skip project, already read)
|
|
245
|
+
for (let i = 1; i < dirs.length; i++) {
|
|
246
|
+
const dir = dirs[i];
|
|
247
|
+
const id = ancestorIds[i - 1];
|
|
248
|
+
const doc = this._readIfExists(path.join(dir, 'doc.md'));
|
|
249
|
+
const ctx = this._readIfExists(path.join(dir, 'context.md'));
|
|
250
|
+
const work = this._readIfExists(path.join(dir, 'work.json'));
|
|
251
|
+
|
|
252
|
+
if (ctx) parts.push(`# Context: ${id}\n\n${ctx}`);
|
|
253
|
+
if (doc) parts.push(`# Documentation: ${id}\n\n${doc}`);
|
|
254
|
+
|
|
255
|
+
// Include work.json acceptance criteria for the task itself
|
|
256
|
+
if (i === dirs.length - 1 && work) {
|
|
257
|
+
try {
|
|
258
|
+
const w = JSON.parse(work);
|
|
259
|
+
if (w.acceptance?.length) {
|
|
260
|
+
parts.push(`# Acceptance Criteria (${id})\n\n${w.acceptance.map((a, j) => `${j + 1}. ${a}`).join('\n')}`);
|
|
261
|
+
}
|
|
262
|
+
} catch {}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Also read subtask docs if they exist
|
|
267
|
+
const taskDir = dirs[dirs.length - 1];
|
|
268
|
+
if (fs.existsSync(taskDir)) {
|
|
269
|
+
try {
|
|
270
|
+
const entries = fs.readdirSync(taskDir, { withFileTypes: true });
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.isDirectory() && entry.name.startsWith('context-')) {
|
|
273
|
+
const subtaskDoc = this._readIfExists(path.join(taskDir, entry.name, 'doc.md'));
|
|
274
|
+
const subtaskCtx = this._readIfExists(path.join(taskDir, entry.name, 'context.md'));
|
|
275
|
+
if (subtaskCtx) parts.push(`# Subtask Context: ${entry.name}\n\n${subtaskCtx}`);
|
|
276
|
+
if (subtaskDoc) parts.push(`# Subtask Documentation: ${entry.name}\n\n${subtaskDoc}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.debug('Doc chain read', { levels: dirs.length, totalChars: parts.join('').length });
|
|
283
|
+
return parts.join('\n\n---\n\n');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get or create an LLM provider for a specific stage.
|
|
288
|
+
*/
|
|
289
|
+
async _getStageProvider(stageName) {
|
|
290
|
+
const key = stageName;
|
|
291
|
+
if (this._stageProviders[key]) return this._stageProviders[key];
|
|
292
|
+
|
|
293
|
+
const stageConfig = this._ceremony?.stages?.[stageName] || {};
|
|
294
|
+
const provider = stageConfig.provider || this._defaultProvider;
|
|
295
|
+
const model = stageConfig.model || this._defaultModel;
|
|
296
|
+
|
|
297
|
+
const instance = await LLMProvider.create(provider, model);
|
|
298
|
+
this._stageProviders[key] = instance;
|
|
299
|
+
return instance;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Load code check definitions from src/cli/checks/code/*.json,
|
|
304
|
+
* with project-level overrides from .avc/customized-agents/checks/code/*.json.
|
|
305
|
+
*/
|
|
306
|
+
_loadCodeChecks() {
|
|
307
|
+
const builtinDir = path.join(__dirname, 'checks', 'code');
|
|
308
|
+
const overrideDir = path.join(this.avcPath, 'customized-agents', 'checks', 'code');
|
|
309
|
+
const checks = [];
|
|
310
|
+
|
|
311
|
+
if (!fs.existsSync(builtinDir)) return checks;
|
|
312
|
+
|
|
313
|
+
for (const file of fs.readdirSync(builtinDir)) {
|
|
314
|
+
if (!file.endsWith('.json')) continue;
|
|
315
|
+
const overridePath = path.join(overrideDir, file);
|
|
316
|
+
const builtinPath = path.join(builtinDir, file);
|
|
317
|
+
const source = fs.existsSync(overridePath) ? overridePath : builtinPath;
|
|
318
|
+
try {
|
|
319
|
+
const defs = JSON.parse(fs.readFileSync(source, 'utf8'));
|
|
320
|
+
checks.push(...defs);
|
|
321
|
+
} catch {}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
this.debug('Code checks loaded', { count: checks.length });
|
|
325
|
+
return checks;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read acceptance criteria from the task's work.json.
|
|
330
|
+
*/
|
|
331
|
+
_readAcceptanceCriteria() {
|
|
332
|
+
const idParts = this.taskId.replace('context-', '').split('-');
|
|
333
|
+
let dir = path.join(this.avcPath, 'project');
|
|
334
|
+
let current = 'context';
|
|
335
|
+
for (const part of idParts) {
|
|
336
|
+
current += `-${part}`;
|
|
337
|
+
dir = path.join(dir, current);
|
|
338
|
+
}
|
|
339
|
+
const workJsonPath = path.join(dir, 'work.json');
|
|
340
|
+
try {
|
|
341
|
+
const w = JSON.parse(fs.readFileSync(workJsonPath, 'utf8'));
|
|
342
|
+
return w.acceptance || [];
|
|
343
|
+
} catch { return []; }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Generate code using the code-implementer agent.
|
|
348
|
+
*/
|
|
349
|
+
async _generateCode(context, prefix, acceptance, violations = null) {
|
|
350
|
+
const provider = await this._getStageProvider('code-generation');
|
|
351
|
+
const agentInstructions = loadAgent('code-implementer.md');
|
|
352
|
+
|
|
353
|
+
const violationSection = violations
|
|
354
|
+
? `\n\n## Previous Validation Violations (FIX THESE)\n\n${violations.map(v => `- [${v.id}] ${v.detail}: ${v.fix}`).join('\n')}`
|
|
355
|
+
: '';
|
|
356
|
+
|
|
357
|
+
const prompt = `## Hierarchy Prefix\n${prefix}\n\n## Task ID\n${this.taskId}\n\n## Acceptance Criteria\n${acceptance.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}\n\n## Documentation Chain\n\n${context}${violationSection}`;
|
|
358
|
+
|
|
359
|
+
const result = await provider.generateJSON(prompt, agentInstructions);
|
|
360
|
+
|
|
361
|
+
if (!result?.files || !Array.isArray(result.files)) {
|
|
362
|
+
throw new Error('Code implementer did not return valid file structure');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Validate generated code using the code-validator agent.
|
|
370
|
+
*/
|
|
371
|
+
async _validateCode(codeOutput, checks, prefix, acceptance) {
|
|
372
|
+
const provider = await this._getStageProvider('code-validation');
|
|
373
|
+
const agentInstructions = loadAgent('code-validator.md');
|
|
374
|
+
|
|
375
|
+
const codeSection = [
|
|
376
|
+
...(codeOutput.files || []).map(f => `### ${f.path}\n\`\`\`javascript\n${f.content}\n\`\`\``),
|
|
377
|
+
...(codeOutput.tests || []).map(f => `### ${f.path}\n\`\`\`javascript\n${f.content}\n\`\`\``),
|
|
378
|
+
].join('\n\n');
|
|
379
|
+
|
|
380
|
+
const prompt = `## Generated Code\n\n${codeSection}\n\n## Check Definitions\n\n${JSON.stringify(checks, null, 2)}\n\n## Task ID\n${this.taskId}\n\n## Hierarchy Prefix\n${prefix}\n\n## Acceptance Criteria\n${acceptance.map((ac, i) => `${i + 1}. ${ac}`).join('\n')}\n\n## Acceptance Threshold\n${this._acceptanceThreshold}`;
|
|
381
|
+
|
|
382
|
+
const result = await provider.generateJSON(prompt, agentInstructions);
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Implement code with generate→validate loop.
|
|
388
|
+
* Replaces the old single-shot implementCode().
|
|
389
|
+
*/
|
|
390
|
+
async implementCode(context, progressCallback) {
|
|
391
|
+
const checks = this._loadCodeChecks();
|
|
392
|
+
const acceptance = this._readAcceptanceCriteria();
|
|
393
|
+
|
|
394
|
+
let codeOutput = null;
|
|
395
|
+
let validationResult = null;
|
|
396
|
+
let violations = null;
|
|
397
|
+
let finalScore = null;
|
|
398
|
+
let bestOutput = null;
|
|
399
|
+
let bestScore = 0;
|
|
400
|
+
|
|
401
|
+
for (let iter = 1; iter <= this._maxIterations; iter++) {
|
|
402
|
+
progressCallback?.(`Code generation (iteration ${iter}/${this._maxIterations})...`);
|
|
403
|
+
codeOutput = await this._generateCode(context, this.prefix, acceptance, violations);
|
|
404
|
+
|
|
405
|
+
if (checks.length > 0) {
|
|
406
|
+
progressCallback?.(`Code validation (iteration ${iter}/${this._maxIterations})...`);
|
|
407
|
+
validationResult = await this._validateCode(codeOutput, checks, this.prefix, acceptance);
|
|
408
|
+
|
|
409
|
+
const score = validationResult?.score ?? 0;
|
|
410
|
+
finalScore = score;
|
|
411
|
+
|
|
412
|
+
// Track best output
|
|
413
|
+
if (score > bestScore) {
|
|
414
|
+
bestScore = score;
|
|
415
|
+
bestOutput = JSON.parse(JSON.stringify(codeOutput));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (validationResult?.passed || score >= this._acceptanceThreshold) {
|
|
419
|
+
progressCallback?.(`Validation passed (score: ${score}, threshold: ${this._acceptanceThreshold})`);
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
violations = validationResult?.violations || [];
|
|
424
|
+
const criticalCount = violations.filter(v => v.severity === 'critical').length;
|
|
425
|
+
progressCallback?.(`Validation: ${violations.length} violation(s) (${criticalCount} critical), score ${score} (threshold: ${this._acceptanceThreshold})`);
|
|
426
|
+
|
|
427
|
+
if (iter === this._maxIterations) {
|
|
428
|
+
// Use best output if final iteration didn't improve
|
|
429
|
+
if (bestOutput && bestScore > score) {
|
|
430
|
+
codeOutput = bestOutput;
|
|
431
|
+
finalScore = bestScore;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Fail if critical violations remain after all iterations
|
|
435
|
+
if (criticalCount > 0 && finalScore < this._acceptanceThreshold) {
|
|
436
|
+
progressCallback?.(`FAILED: ${criticalCount} critical violation(s) remain after ${this._maxIterations} iterations (score: ${finalScore})`);
|
|
437
|
+
throw new Error(`Code validation failed with ${criticalCount} critical violation(s) after ${this._maxIterations} iterations. Score: ${finalScore}/${this._acceptanceThreshold}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
progressCallback?.(`Max iterations — accepting best score ${finalScore} (threshold: ${this._acceptanceThreshold})`);
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
break; // No checks defined, skip validation
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Write all files to worktree
|
|
448
|
+
const allFiles = [...(codeOutput.files || []), ...(codeOutput.tests || [])];
|
|
449
|
+
for (const file of allFiles) {
|
|
450
|
+
if (!file.path || typeof file.content !== 'string') continue;
|
|
451
|
+
const filePath = path.join(this.worktreePath, file.path);
|
|
452
|
+
const fileDir = path.dirname(filePath);
|
|
453
|
+
if (!fs.existsSync(fileDir)) fs.mkdirSync(fileDir, { recursive: true });
|
|
454
|
+
fs.writeFileSync(filePath, file.content, 'utf8');
|
|
455
|
+
this.debug('File written', { path: file.path, size: file.content.length });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Write function registry to task work.json
|
|
459
|
+
if (codeOutput.functionRegistry?.length > 0) {
|
|
460
|
+
this._updateFunctionRegistry(codeOutput.functionRegistry);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Write validation metadata to task work.json
|
|
464
|
+
if (finalScore !== null) {
|
|
465
|
+
this._writeValidationMetadata(finalScore, validationResult?.violations || []);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
progressCallback?.(`Generated ${allFiles.length} file(s) (validation score: ${finalScore ?? 'n/a'}): ${codeOutput.summary || ''}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Write function registry to the task's work.json and propagate up to story and epic.
|
|
473
|
+
*/
|
|
474
|
+
_updateFunctionRegistry(functions) {
|
|
475
|
+
const idParts = this.taskId.replace('context-', '').split('-');
|
|
476
|
+
|
|
477
|
+
// Build path to task's work.json
|
|
478
|
+
let dir = path.join(this.avcPath, 'project');
|
|
479
|
+
let current = 'context';
|
|
480
|
+
for (const part of idParts) {
|
|
481
|
+
current += `-${part}`;
|
|
482
|
+
dir = path.join(dir, current);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Write to task
|
|
486
|
+
this._mergeFunctionsIntoWorkJson(path.join(dir, 'work.json'), functions);
|
|
487
|
+
|
|
488
|
+
// Propagate to story (parent of task)
|
|
489
|
+
const storyDir = path.dirname(dir);
|
|
490
|
+
const storyFunctions = functions.map(f => ({ ...f, task: this.taskId }));
|
|
491
|
+
this._mergeFunctionsIntoWorkJson(path.join(storyDir, 'work.json'), storyFunctions);
|
|
492
|
+
|
|
493
|
+
// Propagate to epic (parent of story)
|
|
494
|
+
const epicDir = path.dirname(storyDir);
|
|
495
|
+
this._mergeFunctionsIntoWorkJson(path.join(epicDir, 'work.json'), storyFunctions);
|
|
496
|
+
|
|
497
|
+
this.debug('Function registry updated', { count: functions.length, taskId: this.taskId });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Merge functions into a work.json file's functions array (deduplicating by name).
|
|
502
|
+
*/
|
|
503
|
+
_mergeFunctionsIntoWorkJson(workJsonPath, newFunctions) {
|
|
504
|
+
if (!fs.existsSync(workJsonPath)) return;
|
|
505
|
+
try {
|
|
506
|
+
const workJson = JSON.parse(fs.readFileSync(workJsonPath, 'utf8'));
|
|
507
|
+
const existing = workJson.functions || [];
|
|
508
|
+
const existingNames = new Set(existing.map(f => f.name));
|
|
509
|
+
for (const fn of newFunctions) {
|
|
510
|
+
if (!existingNames.has(fn.name)) {
|
|
511
|
+
existing.push(fn);
|
|
512
|
+
existingNames.add(fn.name);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
workJson.functions = existing;
|
|
516
|
+
fs.writeFileSync(workJsonPath, JSON.stringify(workJson, null, 2), 'utf8');
|
|
517
|
+
} catch {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Write validation score and remaining violations to the task's work.json metadata.
|
|
522
|
+
*/
|
|
523
|
+
_writeValidationMetadata(score, violations) {
|
|
524
|
+
const idParts = this.taskId.replace('context-', '').split('-');
|
|
525
|
+
let dir = path.join(this.avcPath, 'project');
|
|
526
|
+
let current = 'context';
|
|
527
|
+
for (const part of idParts) {
|
|
528
|
+
current += `-${part}`;
|
|
529
|
+
dir = path.join(dir, current);
|
|
530
|
+
}
|
|
531
|
+
const workJsonPath = path.join(dir, 'work.json');
|
|
532
|
+
if (!fs.existsSync(workJsonPath)) return;
|
|
533
|
+
try {
|
|
534
|
+
const workJson = JSON.parse(fs.readFileSync(workJsonPath, 'utf8'));
|
|
535
|
+
workJson.metadata = workJson.metadata || {};
|
|
536
|
+
workJson.metadata.codeValidation = {
|
|
537
|
+
score,
|
|
538
|
+
threshold: this._acceptanceThreshold,
|
|
539
|
+
violations: violations.length,
|
|
540
|
+
criticalViolations: violations.filter(v => v.severity === 'critical').length,
|
|
541
|
+
lastChecked: new Date().toISOString(),
|
|
542
|
+
};
|
|
543
|
+
fs.writeFileSync(workJsonPath, JSON.stringify(workJson, null, 2), 'utf8');
|
|
544
|
+
} catch {}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Run the project's test command.
|
|
549
|
+
*/
|
|
550
|
+
runTests() {
|
|
551
|
+
// Try to find test command from package.json or avc.json
|
|
552
|
+
let testCmd = 'npm test';
|
|
553
|
+
try {
|
|
554
|
+
const avcConfig = JSON.parse(fs.readFileSync(path.join(this.avcPath, 'avc.json'), 'utf8'));
|
|
555
|
+
if (avcConfig.settings?.testCommand) {
|
|
556
|
+
testCmd = avcConfig.settings.testCommand;
|
|
557
|
+
}
|
|
558
|
+
} catch {}
|
|
559
|
+
|
|
560
|
+
// Check if package.json exists in worktree
|
|
561
|
+
const pkgPath = path.join(this.worktreePath, 'package.json');
|
|
562
|
+
if (!fs.existsSync(pkgPath)) {
|
|
563
|
+
this.debug('No package.json in worktree — skipping tests');
|
|
564
|
+
return { passed: true, summary: 'No test configuration found — skipped' };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const output = execSync(testCmd, {
|
|
569
|
+
cwd: this.worktreePath,
|
|
570
|
+
encoding: 'utf8',
|
|
571
|
+
timeout: 5 * 60_000, // 5 min timeout
|
|
572
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
573
|
+
});
|
|
574
|
+
this.debug('Tests passed', { output: output.slice(-500) });
|
|
575
|
+
return { passed: true, summary: 'All tests passed' };
|
|
576
|
+
} catch (err) {
|
|
577
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
578
|
+
this.debug('Tests failed', { output: output.slice(-1000) });
|
|
579
|
+
return { passed: false, summary: output.slice(-500) };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Commit changes in the worktree and merge to the main branch.
|
|
585
|
+
*/
|
|
586
|
+
commitAndMerge() {
|
|
587
|
+
// Stage all changes
|
|
588
|
+
git(['add', '-A'], this.worktreePath);
|
|
589
|
+
|
|
590
|
+
// Check if there's anything to commit
|
|
591
|
+
const status = git(['status', '--porcelain'], this.worktreePath);
|
|
592
|
+
if (!status) {
|
|
593
|
+
this.debug('No changes to commit');
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Commit
|
|
598
|
+
const commitMsg = `feat(${this.taskId}): implement task\n\nGenerated by AVC WorktreeRunner`;
|
|
599
|
+
git(['commit', '-m', commitMsg], this.worktreePath);
|
|
600
|
+
this.debug('Committed in worktree');
|
|
601
|
+
|
|
602
|
+
// Determine the main branch name
|
|
603
|
+
let mainBranch = 'main';
|
|
604
|
+
try {
|
|
605
|
+
mainBranch = git(['symbolic-ref', '--short', 'HEAD'], this.projectRoot);
|
|
606
|
+
} catch {
|
|
607
|
+
// If HEAD is detached, try common branch names
|
|
608
|
+
try { git(['rev-parse', '--verify', 'main'], this.projectRoot); mainBranch = 'main'; } catch {
|
|
609
|
+
try { git(['rev-parse', '--verify', 'master'], this.projectRoot); mainBranch = 'master'; } catch {}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Merge into main
|
|
614
|
+
try {
|
|
615
|
+
git(['merge', '--no-ff', this.branchName, '-m', `Merge ${this.branchName}: implement ${this.taskId}`], this.projectRoot);
|
|
616
|
+
this.debug('Merged to main', { mainBranch });
|
|
617
|
+
} catch (err) {
|
|
618
|
+
// Merge conflict — abort and report
|
|
619
|
+
try { git(['merge', '--abort'], this.projectRoot); } catch {}
|
|
620
|
+
throw new Error(`Merge conflict: ${err.message}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Clean up the worktree and branch.
|
|
626
|
+
*/
|
|
627
|
+
cleanup() {
|
|
628
|
+
try {
|
|
629
|
+
if (fs.existsSync(this.worktreePath)) {
|
|
630
|
+
git(['worktree', 'remove', '--force', this.worktreePath], this.projectRoot);
|
|
631
|
+
}
|
|
632
|
+
} catch (err) {
|
|
633
|
+
this.debug('Worktree removal warning', { error: err.message });
|
|
634
|
+
// Force remove the directory if git worktree remove fails
|
|
635
|
+
try { fs.rmSync(this.worktreePath, { recursive: true, force: true }); } catch {}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
try {
|
|
639
|
+
git(['branch', '-D', this.branchName], this.projectRoot);
|
|
640
|
+
} catch {
|
|
641
|
+
// Branch may not exist
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
this.debug('Cleanup complete');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
_readIfExists(filePath) {
|
|
648
|
+
try {
|
|
649
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
650
|
+
} catch {
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|