@agile-vibe-coding/avc 0.2.3 → 0.3.2

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.
Files changed (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. 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
+ }